@mcesystems/usb-device-listener 1.0.77 → 1.0.79
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 +50 -2
- package/dist/device-filter.d.ts +3 -2
- package/dist/device-filter.d.ts.map +1 -1
- package/dist/formatPortPath.d.ts +10 -0
- package/dist/formatPortPath.d.ts.map +1 -0
- package/dist/index.d.ts +4 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +221 -7
- package/dist/index.js.map +4 -4
- package/dist/index.mjs +216 -7
- package/dist/index.mjs.map +4 -4
- package/dist/registry.d.ts +29 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/types.d.ts +79 -1
- package/dist/types.d.ts.map +1 -1
- package/native/addon.cc +14 -7
- package/native/usb_listener_common.h +3 -0
- package/native/usb_listener_win.cc +105 -0
- package/native/usb_listener_win.h +2 -0
- package/package.json +3 -2
- package/prebuilds/win32-x64/@mcesystems+usb-device-listener.node +0 -0
package/README.md
CHANGED
|
@@ -114,6 +114,43 @@ usb:detail
|
|
|
114
114
|
usb:detail ================================================================================
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
## Application ID registry (multiple configs)
|
|
118
|
+
|
|
119
|
+
When multiple parts of the same process need **separate** USB device filters and callbacks (e.g. one app for Samsung devices, another for Arduino), use `UsbDeviceListenerRegistry`. You register an application ID with a config, get a scoped listener for that ID, and only receive events that match that app's config. One native listener is shared; the registry fans out events per app.
|
|
120
|
+
|
|
121
|
+
**When to use:**
|
|
122
|
+
- Multiple logical "apps" or modules in one process, each with its own `ListenerConfig`
|
|
123
|
+
- You want each app to call `onDeviceAdd` / `onDeviceRemove` and only see devices matching its filters
|
|
124
|
+
- You want `listDevices()` per app to return only devices that match that app's config
|
|
125
|
+
|
|
126
|
+
**Example:** See [src/examples/example-registry.ts](src/examples/example-registry.ts). Run with `pnpm example:registry`.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
import { UsbDeviceListenerRegistry } from "@mcesystems/usb-device-listener";
|
|
130
|
+
|
|
131
|
+
const registry = new UsbDeviceListenerRegistry();
|
|
132
|
+
registry.register("app-samsung", { targetDevices: [{ vid: "04E8", pid: "6860" }] });
|
|
133
|
+
registry.register("app-arduino", { targetDevices: [{ vid: "2341", pid: "0043" }] });
|
|
134
|
+
|
|
135
|
+
const listenerSamsung = registry.getListener("app-samsung");
|
|
136
|
+
const listenerArduino = registry.getListener("app-arduino");
|
|
137
|
+
|
|
138
|
+
listenerSamsung.onDeviceAdd((device) => { /* only Samsung devices */ });
|
|
139
|
+
listenerArduino.onDeviceAdd((device) => { /* only Arduino devices */ });
|
|
140
|
+
|
|
141
|
+
listenerSamsung.startListening({ targetDevices: [{ vid: "04E8", pid: "6860" }] });
|
|
142
|
+
listenerArduino.startListening({ targetDevices: [{ vid: "2341", pid: "0043" }] });
|
|
143
|
+
|
|
144
|
+
// Later: unregister and stop native listener when last app unregisters
|
|
145
|
+
registry.unregister("app-samsung");
|
|
146
|
+
registry.unregister("app-arduino");
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Registry API:**
|
|
150
|
+
- `register(appId, config)` — Register or update config for an application ID.
|
|
151
|
+
- `getListener(appId)` — Return a listener (implements `UsbDeviceListenerI`) scoped to that app; events and `listDevices()` are filtered by the app's config.
|
|
152
|
+
- `unregister(appId)` — Remove the app and stop the native listener if it was the last one started.
|
|
153
|
+
|
|
117
154
|
## API Reference
|
|
118
155
|
|
|
119
156
|
### `startListening(config)`
|
|
@@ -125,6 +162,10 @@ Start monitoring USB device events.
|
|
|
125
162
|
- `logicalPortMap` (Object, optional): Map physical locations to logical port numbers
|
|
126
163
|
- Key: Location string (platform-specific format)
|
|
127
164
|
- Value: Logical port number (integer)
|
|
165
|
+
- `logicalPortMapByHub` (Object, optional): Map physical port to logical port per hub (alternative to logicalPortMap)
|
|
166
|
+
- Key: Hub identifier `"${vid}-${pid}"` (hex, uppercase 4-char, e.g. `"1A2C-3B4D"`)
|
|
167
|
+
- Value: Object mapping physical port number to logical port number (e.g. `{ 1: 4, 4: 2, 3: 3, 2: 1 }`)
|
|
168
|
+
- When both `logicalPortMap` and `logicalPortMapByHub` could apply, `logicalPortMapByHub` takes precedence when the device has hub data (`portPath`, `parentHubVid`, `parentHubPid`)
|
|
128
169
|
- `targetDevices` (Array, optional): Filter specific devices by VID/PID
|
|
129
170
|
- Each element: `{ vid: string, pid: string }` (hex strings, e.g., "04E8")
|
|
130
171
|
- Empty array = monitor all devices
|
|
@@ -203,6 +244,9 @@ Register callback for device connection events.
|
|
|
203
244
|
- `pid` (number): Product ID (decimal)
|
|
204
245
|
- `locationInfo` (string): Physical port location (platform-specific format)
|
|
205
246
|
- `logicalPort` (number|null): Mapped logical port or null
|
|
247
|
+
- `portPath` (number[]|undefined): Port path from root to device (e.g. [2, 3, 1]); present when native layer provides it (e.g. Windows)
|
|
248
|
+
- `parentHubVid` (number|undefined): VID of hub the device is directly connected to (0 if on root)
|
|
249
|
+
- `parentHubPid` (number|undefined): PID of that hub
|
|
206
250
|
|
|
207
251
|
**Example:**
|
|
208
252
|
```javascript
|
|
@@ -277,18 +321,20 @@ Use the included `list-devices.js` utility:
|
|
|
277
321
|
node list-devices.js
|
|
278
322
|
```
|
|
279
323
|
|
|
280
|
-
**Windows output
|
|
324
|
+
**Windows output** (when port path is available, `Port path (tree)` shows the chain, e.g. 2/3/1 = port 2, then 3, then 1):
|
|
281
325
|
```
|
|
282
326
|
Device 1:
|
|
283
327
|
Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
|
|
284
328
|
VID: 0x04E8
|
|
285
329
|
PID: 0x6860
|
|
330
|
+
Port path (tree): 2/3/1
|
|
286
331
|
Location Info (mapping key): Port_#0005.Hub_#0002
|
|
287
332
|
|
|
288
333
|
Device 2:
|
|
289
334
|
Device ID: USB\VID_27C6&PID_6594\UID0014C59F
|
|
290
335
|
VID: 0x27C6
|
|
291
336
|
PID: 0x6594
|
|
337
|
+
Port path (tree): 2/7
|
|
292
338
|
Location Info (mapping key): Port_#0007.Hub_#0002
|
|
293
339
|
```
|
|
294
340
|
|
|
@@ -307,7 +353,9 @@ Device 2:
|
|
|
307
353
|
Location Info (mapping key): Port_#14100000
|
|
308
354
|
```
|
|
309
355
|
|
|
310
|
-
Copy the "Location Info" values to use in your `logicalPortMap`.
|
|
356
|
+
Copy the "Location Info" values to use in your `logicalPortMap`. When available, use **Port path (tree)** and the parent hub (from list output or `parentHubVid`/`parentHubPid`) with `logicalPortMapByHub`.
|
|
357
|
+
|
|
358
|
+
**Helper:** `formatPortPath(device)` returns the port path as a string (e.g. `"2/3/1"`) or falls back to `locationInfo` when port path is not available.
|
|
311
359
|
|
|
312
360
|
### Device Filtering
|
|
313
361
|
|
package/dist/device-filter.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { DeviceInfo, ListenerConfig } from "./types";
|
|
|
6
6
|
* 1. ignoredDevices - if device matches, always return false
|
|
7
7
|
* 2. listenOnlyDevices - if specified, device must match at least one
|
|
8
8
|
* 3. targetDevices - if specified, device must match at least one
|
|
9
|
-
* 4. logicalPortMap - if specified, device
|
|
9
|
+
* 4. logicalPortMap / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)
|
|
10
10
|
*
|
|
11
11
|
* @param device - The device information from the native addon
|
|
12
12
|
* @param config - The listener configuration
|
|
@@ -14,7 +14,8 @@ import type { DeviceInfo, ListenerConfig } from "./types";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean;
|
|
16
16
|
/**
|
|
17
|
-
* Apply logical port mapping to a device if configured
|
|
17
|
+
* Apply logical port mapping to a device if configured.
|
|
18
|
+
* logicalPortMapByHub takes precedence when device has parent hub and port path.
|
|
18
19
|
*
|
|
19
20
|
* @param device - The device information from the native addon
|
|
20
21
|
* @param config - The listener configuration
|
|
@@ -1 +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;
|
|
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;AAoDxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CA6CtF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CA2B9F"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DeviceInfo } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Format device port path for display (e.g. "2/3/1" for port 2, then 3, then 1).
|
|
4
|
+
* Uses native portPath when available; otherwise returns locationInfo.
|
|
5
|
+
*
|
|
6
|
+
* @param device - Device info from the listener
|
|
7
|
+
* @returns Port path string (e.g. "2/3/1") or locationInfo when portPath is missing
|
|
8
|
+
*/
|
|
9
|
+
export declare function formatPortPath(device: DeviceInfo): string;
|
|
10
|
+
//# sourceMappingURL=formatPortPath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatPortPath.d.ts","sourceRoot":"","sources":["../src/formatPortPath.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAKzD"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, UsbDeviceListenerI } from "./types";
|
|
2
|
-
export interface NativeAddon {
|
|
3
|
-
startListening(): void;
|
|
4
|
-
stopListening(): void;
|
|
5
|
-
onDeviceAdd(callback: DeviceAddCallback): void;
|
|
6
|
-
onDeviceRemove(callback: DeviceRemoveCallback): void;
|
|
7
|
-
listDevices(): DeviceInfo[];
|
|
8
|
-
}
|
|
1
|
+
import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, NativeAddon, UsbDeviceListenerI } from "./types";
|
|
9
2
|
/**
|
|
10
3
|
* USB Device Listener implementation
|
|
11
4
|
* Provides a type-safe wrapper around the native C++ addon
|
|
@@ -49,5 +42,7 @@ export default class UsbDeviceListener implements UsbDeviceListenerI {
|
|
|
49
42
|
*/
|
|
50
43
|
listDevices(): DeviceInfo[];
|
|
51
44
|
}
|
|
52
|
-
export
|
|
45
|
+
export { formatPortPath } from "./formatPortPath.js";
|
|
46
|
+
export { UsbDeviceListenerRegistry } from "./registry.js";
|
|
47
|
+
export type { DeviceInfo, DeviceAddCallback, DeviceRemoveCallback, ListenerConfig, NativeAddon, TargetDevice, UsbDeviceListenerRegistryI, } from "./types";
|
|
53
48
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,MAAM,SAAS,CAAC;AAUjB;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IACnE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,kBAAkB,CAAqC;gBAEnD,KAAK,CAAC,EAAE,WAAW;IAI/B;;OAEG;IACI,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQnD;;OAEG;IACI,aAAa,IAAI,IAAI;IAI5B;;;;OAIG;IACI,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAOjD;;;OAGG;IACI,gBAAgB,IAAI,cAAc;IAIzC;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAerD;;OAEG;IACI,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAe3D;;;OAGG;IACI,WAAW,IAAI,UAAU,EAAE;CAKlC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,YAAY,EACZ,0BAA0B,GAC1B,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -31,12 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
-
|
|
34
|
+
UsbDeviceListenerRegistry: () => UsbDeviceListenerRegistry,
|
|
35
|
+
default: () => UsbDeviceListener,
|
|
36
|
+
formatPortPath: () => formatPortPath
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
38
|
-
var
|
|
39
|
-
var
|
|
39
|
+
var import_node_module2 = require("node:module");
|
|
40
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
41
|
+
var import_node_url2 = require("node:url");
|
|
40
42
|
|
|
41
43
|
// src/device-filter.ts
|
|
42
44
|
function toHexString(value) {
|
|
@@ -52,6 +54,22 @@ function matchesDevice(device, target) {
|
|
|
52
54
|
function matchesAnyDevice(device, targets) {
|
|
53
55
|
return targets.some((target) => matchesDevice(device, target));
|
|
54
56
|
}
|
|
57
|
+
function hubKeyFromParent(vid, pid) {
|
|
58
|
+
return `${vid.toString(16).toUpperCase().padStart(4, "0")}-${pid.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
59
|
+
}
|
|
60
|
+
function isMappedInLogicalPortMapByHub(device, config) {
|
|
61
|
+
const map = config.logicalPortMapByHub;
|
|
62
|
+
if (!map || Object.keys(map).length === 0) return false;
|
|
63
|
+
const vid = device.parentHubVid;
|
|
64
|
+
const pid = device.parentHubPid;
|
|
65
|
+
const path3 = device.portPath;
|
|
66
|
+
if (vid === void 0 || pid === void 0 || !path3 || path3.length === 0) return false;
|
|
67
|
+
const key = hubKeyFromParent(vid, pid);
|
|
68
|
+
const portMap = map[key];
|
|
69
|
+
if (!portMap) return false;
|
|
70
|
+
const physicalPort = path3[path3.length - 1];
|
|
71
|
+
return physicalPort in portMap;
|
|
72
|
+
}
|
|
55
73
|
function shouldNotifyDevice(device, config) {
|
|
56
74
|
if (config.ignoredDevices && config.ignoredDevices.length > 0) {
|
|
57
75
|
if (matchesAnyDevice(device, config.ignoredDevices)) {
|
|
@@ -68,14 +86,34 @@ function shouldNotifyDevice(device, config) {
|
|
|
68
86
|
return false;
|
|
69
87
|
}
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
const hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;
|
|
90
|
+
const hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;
|
|
91
|
+
if (hasHubMap && device.parentHubVid !== void 0 && device.parentHubPid !== void 0 && device.portPath && device.portPath.length > 0) {
|
|
92
|
+
if (!isMappedInLogicalPortMapByHub(device, config)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
} else if (hasLocationMap && config.logicalPortMap) {
|
|
72
96
|
if (!(device.locationInfo in config.logicalPortMap)) {
|
|
73
97
|
return false;
|
|
74
98
|
}
|
|
99
|
+
} else if (hasHubMap) {
|
|
100
|
+
return false;
|
|
75
101
|
}
|
|
76
102
|
return true;
|
|
77
103
|
}
|
|
78
104
|
function applyLogicalPortMapping(device, config) {
|
|
105
|
+
const mapByHub = config.logicalPortMapByHub;
|
|
106
|
+
if (mapByHub && Object.keys(mapByHub).length > 0 && device.parentHubVid !== void 0 && device.parentHubPid !== void 0 && device.portPath && device.portPath.length > 0) {
|
|
107
|
+
const key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);
|
|
108
|
+
const portMap = mapByHub[key];
|
|
109
|
+
const physicalPort = device.portPath[device.portPath.length - 1];
|
|
110
|
+
if (portMap && physicalPort in portMap) {
|
|
111
|
+
return {
|
|
112
|
+
...device,
|
|
113
|
+
logicalPort: portMap[physicalPort]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
79
117
|
if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
|
|
80
118
|
return {
|
|
81
119
|
...device,
|
|
@@ -85,7 +123,18 @@ function applyLogicalPortMapping(device, config) {
|
|
|
85
123
|
return device;
|
|
86
124
|
}
|
|
87
125
|
|
|
88
|
-
// src/
|
|
126
|
+
// src/formatPortPath.ts
|
|
127
|
+
function formatPortPath(device) {
|
|
128
|
+
if (device.portPath && device.portPath.length > 0) {
|
|
129
|
+
return device.portPath.join("/");
|
|
130
|
+
}
|
|
131
|
+
return device.locationInfo;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/registry.ts
|
|
135
|
+
var import_node_module = require("node:module");
|
|
136
|
+
var import_node_path = __toESM(require("node:path"));
|
|
137
|
+
var import_node_url = require("node:url");
|
|
89
138
|
function loadDefaultAddon() {
|
|
90
139
|
const require2 = (0, import_node_module.createRequire)(__importMetaUrl);
|
|
91
140
|
const __filename = (0, import_node_url.fileURLToPath)(__importMetaUrl);
|
|
@@ -93,13 +142,173 @@ function loadDefaultAddon() {
|
|
|
93
142
|
const packageRoot = import_node_path.default.join(__dirname, "..");
|
|
94
143
|
return require2("node-gyp-build")(packageRoot);
|
|
95
144
|
}
|
|
145
|
+
function ensureAddonCallbacksInstalled(addon, apps) {
|
|
146
|
+
addon.onDeviceAdd((device) => {
|
|
147
|
+
for (const entry of apps.values()) {
|
|
148
|
+
if (!entry.started || !entry.addCallback) continue;
|
|
149
|
+
if (shouldNotifyDevice(device, entry.config)) {
|
|
150
|
+
const mapped = applyLogicalPortMapping(device, entry.config);
|
|
151
|
+
entry.addCallback(mapped);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
addon.onDeviceRemove((device) => {
|
|
156
|
+
for (const entry of apps.values()) {
|
|
157
|
+
if (!entry.started || !entry.removeCallback) continue;
|
|
158
|
+
if (shouldNotifyDevice(device, entry.config)) {
|
|
159
|
+
const mapped = applyLogicalPortMapping(device, entry.config);
|
|
160
|
+
entry.removeCallback(mapped);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
var ScopedListener = class {
|
|
166
|
+
constructor(registry, appId) {
|
|
167
|
+
this.registry = registry;
|
|
168
|
+
this.appId = appId;
|
|
169
|
+
}
|
|
170
|
+
getEntry() {
|
|
171
|
+
const entry = this.registry.getEntry(this.appId);
|
|
172
|
+
if (!entry) {
|
|
173
|
+
throw new Error(`App not registered: ${this.appId}`);
|
|
174
|
+
}
|
|
175
|
+
return entry;
|
|
176
|
+
}
|
|
177
|
+
startListening(config) {
|
|
178
|
+
if (typeof config !== "object" || config === null) {
|
|
179
|
+
throw new TypeError("Config must be an object");
|
|
180
|
+
}
|
|
181
|
+
const entry = this.getEntry();
|
|
182
|
+
entry.config = config;
|
|
183
|
+
entry.started = true;
|
|
184
|
+
this.registry.ensureNativeListenerRunning();
|
|
185
|
+
}
|
|
186
|
+
stopListening() {
|
|
187
|
+
const entry = this.getEntry();
|
|
188
|
+
entry.started = false;
|
|
189
|
+
this.registry.maybeStopNativeListener();
|
|
190
|
+
}
|
|
191
|
+
updateConfig(config) {
|
|
192
|
+
if (typeof config !== "object" || config === null) {
|
|
193
|
+
throw new TypeError("Config must be an object");
|
|
194
|
+
}
|
|
195
|
+
this.getEntry().config = config;
|
|
196
|
+
}
|
|
197
|
+
getCurrentConfig() {
|
|
198
|
+
return { ...this.getEntry().config };
|
|
199
|
+
}
|
|
200
|
+
onDeviceAdd(callback) {
|
|
201
|
+
if (typeof callback !== "function") {
|
|
202
|
+
throw new TypeError("Callback must be a function");
|
|
203
|
+
}
|
|
204
|
+
this.getEntry().addCallback = callback;
|
|
205
|
+
this.registry.ensureAddonCallbacksInstalled();
|
|
206
|
+
}
|
|
207
|
+
onDeviceRemove(callback) {
|
|
208
|
+
if (typeof callback !== "function") {
|
|
209
|
+
throw new TypeError("Callback must be a function");
|
|
210
|
+
}
|
|
211
|
+
this.getEntry().removeCallback = callback;
|
|
212
|
+
this.registry.ensureAddonCallbacksInstalled();
|
|
213
|
+
}
|
|
214
|
+
listDevices() {
|
|
215
|
+
const config = this.getEntry().config;
|
|
216
|
+
const devices = this.registry.listDevicesFromAddon();
|
|
217
|
+
return devices.filter((device) => shouldNotifyDevice(device, config)).map((device) => applyLogicalPortMapping(device, config));
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var UsbDeviceListenerRegistry = class {
|
|
221
|
+
apps = /* @__PURE__ */ new Map();
|
|
222
|
+
addon;
|
|
223
|
+
addonCallbacksInstalled = false;
|
|
224
|
+
nativeListenerRunning = false;
|
|
225
|
+
scopedListeners = /* @__PURE__ */ new Map();
|
|
226
|
+
constructor(addon) {
|
|
227
|
+
this.addon = addon ?? loadDefaultAddon();
|
|
228
|
+
}
|
|
229
|
+
getEntry(appId) {
|
|
230
|
+
return this.apps.get(appId);
|
|
231
|
+
}
|
|
232
|
+
ensureNativeListenerRunning() {
|
|
233
|
+
if (this.nativeListenerRunning) return;
|
|
234
|
+
this.nativeListenerRunning = true;
|
|
235
|
+
this.addon.startListening();
|
|
236
|
+
}
|
|
237
|
+
maybeStopNativeListener() {
|
|
238
|
+
const anyStarted = [...this.apps.values()].some((e) => e.started);
|
|
239
|
+
if (!anyStarted && this.nativeListenerRunning) {
|
|
240
|
+
this.nativeListenerRunning = false;
|
|
241
|
+
this.addon.stopListening();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
ensureAddonCallbacksInstalled() {
|
|
245
|
+
if (this.addonCallbacksInstalled) return;
|
|
246
|
+
this.addonCallbacksInstalled = true;
|
|
247
|
+
ensureAddonCallbacksInstalled(this.addon, this.apps);
|
|
248
|
+
}
|
|
249
|
+
listDevicesFromAddon() {
|
|
250
|
+
return this.addon.listDevices();
|
|
251
|
+
}
|
|
252
|
+
register(appId, config) {
|
|
253
|
+
if (typeof config !== "object" || config === null) {
|
|
254
|
+
throw new TypeError("Config must be an object");
|
|
255
|
+
}
|
|
256
|
+
const existing = this.apps.get(appId);
|
|
257
|
+
if (existing) {
|
|
258
|
+
existing.config = config;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.apps.set(appId, {
|
|
262
|
+
config: { ...config },
|
|
263
|
+
addCallback: null,
|
|
264
|
+
removeCallback: null,
|
|
265
|
+
started: false
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
getListener(appId) {
|
|
269
|
+
if (!this.apps.has(appId)) {
|
|
270
|
+
this.apps.set(appId, {
|
|
271
|
+
config: {},
|
|
272
|
+
addCallback: null,
|
|
273
|
+
removeCallback: null,
|
|
274
|
+
started: false
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
let scoped = this.scopedListeners.get(appId);
|
|
278
|
+
if (!scoped) {
|
|
279
|
+
scoped = new ScopedListener(this, appId);
|
|
280
|
+
this.scopedListeners.set(appId, scoped);
|
|
281
|
+
}
|
|
282
|
+
return scoped;
|
|
283
|
+
}
|
|
284
|
+
unregister(appId) {
|
|
285
|
+
this.scopedListeners.delete(appId);
|
|
286
|
+
const entry = this.apps.get(appId);
|
|
287
|
+
if (entry) {
|
|
288
|
+
entry.started = false;
|
|
289
|
+
entry.addCallback = null;
|
|
290
|
+
entry.removeCallback = null;
|
|
291
|
+
this.apps.delete(appId);
|
|
292
|
+
}
|
|
293
|
+
this.maybeStopNativeListener();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/index.ts
|
|
298
|
+
function loadDefaultAddon2() {
|
|
299
|
+
const require2 = (0, import_node_module2.createRequire)(__importMetaUrl);
|
|
300
|
+
const __filename = (0, import_node_url2.fileURLToPath)(__importMetaUrl);
|
|
301
|
+
const __dirname = import_node_path2.default.dirname(__filename);
|
|
302
|
+
const packageRoot = import_node_path2.default.join(__dirname, "..");
|
|
303
|
+
return require2("node-gyp-build")(packageRoot);
|
|
304
|
+
}
|
|
96
305
|
var UsbDeviceListener = class {
|
|
97
306
|
addon;
|
|
98
307
|
config = {};
|
|
99
308
|
userAddCallback = null;
|
|
100
309
|
userRemoveCallback = null;
|
|
101
310
|
constructor(addon) {
|
|
102
|
-
this.addon = addon ??
|
|
311
|
+
this.addon = addon ?? loadDefaultAddon2();
|
|
103
312
|
}
|
|
104
313
|
/**
|
|
105
314
|
* Start listening for USB device events
|
|
@@ -174,5 +383,10 @@ var UsbDeviceListener = class {
|
|
|
174
383
|
return devices.map((device) => applyLogicalPortMapping(device, this.config));
|
|
175
384
|
}
|
|
176
385
|
};
|
|
386
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
387
|
+
0 && (module.exports = {
|
|
388
|
+
UsbDeviceListenerRegistry,
|
|
389
|
+
formatPortPath
|
|
390
|
+
});
|
|
177
391
|
module.exports = Object.assign(module.exports.default, module.exports);
|
|
178
392
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/device-filter.ts"],
|
|
4
|
-
"sourcesContent": ["import { createRequire } from \"node:module\";\nimport path 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\tUsbDeviceListenerI,\n} from \"./types\";\n\nexport interface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\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\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(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}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\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\tthis.addon.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\tthis.addon.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 * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
6
|
-
"names": ["require", "path"]
|
|
3
|
+
"sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts", "../src/registry.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequire } from \"node:module\";\nimport path 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\tNativeAddon,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\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\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(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}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\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\tthis.addon.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\tthis.addon.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 * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport { UsbDeviceListenerRegistry } from \"./registry.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tTargetDevice,\n\tUsbDeviceListenerRegistryI,\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 * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\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 / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\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 / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (\n\t\thasHubMap &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\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\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\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 type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * Uses native portPath when available; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo when portPath is missing\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n", "import { createRequire } from \"node:module\";\nimport path 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\tNativeAddon,\n\tUsbDeviceListenerI,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n\ninterface AppEntry {\n\tconfig: ListenerConfig;\n\taddCallback: DeviceAddCallback | null;\n\tremoveCallback: DeviceRemoveCallback | null;\n\tstarted: boolean;\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\nfunction ensureAddonCallbacksInstalled(addon: NativeAddon, apps: Map<string, AppEntry>): void {\n\t// Install once: single fan-out to all registered apps\n\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.addCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.addCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.removeCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.removeCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Scoped listener that delegates to the registry and uses one app's config.\n */\nclass ScopedListener implements UsbDeviceListenerI {\n\tconstructor(\n\t\tprivate readonly registry: UsbDeviceListenerRegistry,\n\t\tprivate readonly appId: string\n\t) {}\n\n\tprivate getEntry(): AppEntry {\n\t\tconst entry = this.registry.getEntry(this.appId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`App not registered: ${this.appId}`);\n\t\t}\n\t\treturn entry;\n\t}\n\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\tconst entry = this.getEntry();\n\t\tentry.config = config;\n\t\tentry.started = true;\n\t\tthis.registry.ensureNativeListenerRunning();\n\t}\n\n\tpublic stopListening(): void {\n\t\tconst entry = this.getEntry();\n\t\tentry.started = false;\n\t\tthis.registry.maybeStopNativeListener();\n\t}\n\n\tpublic updateConfig(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.getEntry().config = config;\n\t}\n\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.getEntry().config };\n\t}\n\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.getEntry().addCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\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.getEntry().removeCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst config = this.getEntry().config;\n\t\tconst devices = this.registry.listDevicesFromAddon();\n\t\treturn devices\n\t\t\t.filter((device) => shouldNotifyDevice(device, config))\n\t\t\t.map((device) => applyLogicalPortMapping(device, config));\n\t}\n}\n\n/**\n * Registry for multiple application IDs, each with its own listener config.\n * One native listener is shared; events are fanned out per app based on config.\n */\nexport class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {\n\tprivate readonly apps = new Map<string, AppEntry>();\n\tprivate readonly addon: NativeAddon;\n\tprivate addonCallbacksInstalled = false;\n\tprivate nativeListenerRunning = false;\n\tprivate readonly scopedListeners = new Map<string, ScopedListener>();\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\tgetEntry(appId: string): AppEntry | undefined {\n\t\treturn this.apps.get(appId);\n\t}\n\n\tensureNativeListenerRunning(): void {\n\t\tif (this.nativeListenerRunning) return;\n\t\tthis.nativeListenerRunning = true;\n\t\tthis.addon.startListening();\n\t}\n\n\tmaybeStopNativeListener(): void {\n\t\tconst anyStarted = [...this.apps.values()].some((e) => e.started);\n\t\tif (!anyStarted && this.nativeListenerRunning) {\n\t\t\tthis.nativeListenerRunning = false;\n\t\t\tthis.addon.stopListening();\n\t\t}\n\t}\n\n\tensureAddonCallbacksInstalled(): void {\n\t\tif (this.addonCallbacksInstalled) return;\n\t\tthis.addonCallbacksInstalled = true;\n\t\tensureAddonCallbacksInstalled(this.addon, this.apps);\n\t}\n\n\tlistDevicesFromAddon(): DeviceInfo[] {\n\t\treturn this.addon.listDevices();\n\t}\n\n\tpublic register(appId: string, 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\tconst existing = this.apps.get(appId);\n\t\tif (existing) {\n\t\t\texisting.config = config;\n\t\t\treturn;\n\t\t}\n\t\tthis.apps.set(appId, {\n\t\t\tconfig: { ...config },\n\t\t\taddCallback: null,\n\t\t\tremoveCallback: null,\n\t\t\tstarted: false,\n\t\t});\n\t}\n\n\tpublic getListener(appId: string): UsbDeviceListenerI {\n\t\tif (!this.apps.has(appId)) {\n\t\t\tthis.apps.set(appId, {\n\t\t\t\tconfig: {},\n\t\t\t\taddCallback: null,\n\t\t\t\tremoveCallback: null,\n\t\t\t\tstarted: false,\n\t\t\t});\n\t\t}\n\t\tlet scoped = this.scopedListeners.get(appId);\n\t\tif (!scoped) {\n\t\t\tscoped = new ScopedListener(this, appId);\n\t\t\tthis.scopedListeners.set(appId, scoped);\n\t\t}\n\t\treturn scoped;\n\t}\n\n\tpublic unregister(appId: string): void {\n\t\tthis.scopedListeners.delete(appId);\n\t\tconst entry = this.apps.get(appId);\n\t\tif (entry) {\n\t\t\tentry.started = false;\n\t\t\tentry.addCallback = null;\n\t\t\tentry.removeCallback = null;\n\t\t\tthis.apps.delete(appId);\n\t\t}\n\t\tthis.maybeStopNativeListener();\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_node_module", "import_node_path", "import_node_url", "path", "require", "path", "loadDefaultAddon", "require", "path"]
|
|
7
7
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
3
|
+
import path2 from "node:path";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
5
|
|
|
6
6
|
// src/device-filter.ts
|
|
7
7
|
function toHexString(value) {
|
|
@@ -17,6 +17,22 @@ function matchesDevice(device, target) {
|
|
|
17
17
|
function matchesAnyDevice(device, targets) {
|
|
18
18
|
return targets.some((target) => matchesDevice(device, target));
|
|
19
19
|
}
|
|
20
|
+
function hubKeyFromParent(vid, pid) {
|
|
21
|
+
return `${vid.toString(16).toUpperCase().padStart(4, "0")}-${pid.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
22
|
+
}
|
|
23
|
+
function isMappedInLogicalPortMapByHub(device, config) {
|
|
24
|
+
const map = config.logicalPortMapByHub;
|
|
25
|
+
if (!map || Object.keys(map).length === 0) return false;
|
|
26
|
+
const vid = device.parentHubVid;
|
|
27
|
+
const pid = device.parentHubPid;
|
|
28
|
+
const path3 = device.portPath;
|
|
29
|
+
if (vid === void 0 || pid === void 0 || !path3 || path3.length === 0) return false;
|
|
30
|
+
const key = hubKeyFromParent(vid, pid);
|
|
31
|
+
const portMap = map[key];
|
|
32
|
+
if (!portMap) return false;
|
|
33
|
+
const physicalPort = path3[path3.length - 1];
|
|
34
|
+
return physicalPort in portMap;
|
|
35
|
+
}
|
|
20
36
|
function shouldNotifyDevice(device, config) {
|
|
21
37
|
if (config.ignoredDevices && config.ignoredDevices.length > 0) {
|
|
22
38
|
if (matchesAnyDevice(device, config.ignoredDevices)) {
|
|
@@ -33,14 +49,34 @@ function shouldNotifyDevice(device, config) {
|
|
|
33
49
|
return false;
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
|
-
|
|
52
|
+
const hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;
|
|
53
|
+
const hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;
|
|
54
|
+
if (hasHubMap && device.parentHubVid !== void 0 && device.parentHubPid !== void 0 && device.portPath && device.portPath.length > 0) {
|
|
55
|
+
if (!isMappedInLogicalPortMapByHub(device, config)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
} else if (hasLocationMap && config.logicalPortMap) {
|
|
37
59
|
if (!(device.locationInfo in config.logicalPortMap)) {
|
|
38
60
|
return false;
|
|
39
61
|
}
|
|
62
|
+
} else if (hasHubMap) {
|
|
63
|
+
return false;
|
|
40
64
|
}
|
|
41
65
|
return true;
|
|
42
66
|
}
|
|
43
67
|
function applyLogicalPortMapping(device, config) {
|
|
68
|
+
const mapByHub = config.logicalPortMapByHub;
|
|
69
|
+
if (mapByHub && Object.keys(mapByHub).length > 0 && device.parentHubVid !== void 0 && device.parentHubPid !== void 0 && device.portPath && device.portPath.length > 0) {
|
|
70
|
+
const key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);
|
|
71
|
+
const portMap = mapByHub[key];
|
|
72
|
+
const physicalPort = device.portPath[device.portPath.length - 1];
|
|
73
|
+
if (portMap && physicalPort in portMap) {
|
|
74
|
+
return {
|
|
75
|
+
...device,
|
|
76
|
+
logicalPort: portMap[physicalPort]
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
44
80
|
if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
|
|
45
81
|
return {
|
|
46
82
|
...device,
|
|
@@ -50,7 +86,18 @@ function applyLogicalPortMapping(device, config) {
|
|
|
50
86
|
return device;
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
// src/
|
|
89
|
+
// src/formatPortPath.ts
|
|
90
|
+
function formatPortPath(device) {
|
|
91
|
+
if (device.portPath && device.portPath.length > 0) {
|
|
92
|
+
return device.portPath.join("/");
|
|
93
|
+
}
|
|
94
|
+
return device.locationInfo;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/registry.ts
|
|
98
|
+
import { createRequire } from "node:module";
|
|
99
|
+
import path from "node:path";
|
|
100
|
+
import { fileURLToPath } from "node:url";
|
|
54
101
|
function loadDefaultAddon() {
|
|
55
102
|
const require2 = createRequire(import.meta.url);
|
|
56
103
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -58,13 +105,173 @@ function loadDefaultAddon() {
|
|
|
58
105
|
const packageRoot = path.join(__dirname, "..");
|
|
59
106
|
return require2("node-gyp-build")(packageRoot);
|
|
60
107
|
}
|
|
108
|
+
function ensureAddonCallbacksInstalled(addon, apps) {
|
|
109
|
+
addon.onDeviceAdd((device) => {
|
|
110
|
+
for (const entry of apps.values()) {
|
|
111
|
+
if (!entry.started || !entry.addCallback) continue;
|
|
112
|
+
if (shouldNotifyDevice(device, entry.config)) {
|
|
113
|
+
const mapped = applyLogicalPortMapping(device, entry.config);
|
|
114
|
+
entry.addCallback(mapped);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
addon.onDeviceRemove((device) => {
|
|
119
|
+
for (const entry of apps.values()) {
|
|
120
|
+
if (!entry.started || !entry.removeCallback) continue;
|
|
121
|
+
if (shouldNotifyDevice(device, entry.config)) {
|
|
122
|
+
const mapped = applyLogicalPortMapping(device, entry.config);
|
|
123
|
+
entry.removeCallback(mapped);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
var ScopedListener = class {
|
|
129
|
+
constructor(registry, appId) {
|
|
130
|
+
this.registry = registry;
|
|
131
|
+
this.appId = appId;
|
|
132
|
+
}
|
|
133
|
+
getEntry() {
|
|
134
|
+
const entry = this.registry.getEntry(this.appId);
|
|
135
|
+
if (!entry) {
|
|
136
|
+
throw new Error(`App not registered: ${this.appId}`);
|
|
137
|
+
}
|
|
138
|
+
return entry;
|
|
139
|
+
}
|
|
140
|
+
startListening(config) {
|
|
141
|
+
if (typeof config !== "object" || config === null) {
|
|
142
|
+
throw new TypeError("Config must be an object");
|
|
143
|
+
}
|
|
144
|
+
const entry = this.getEntry();
|
|
145
|
+
entry.config = config;
|
|
146
|
+
entry.started = true;
|
|
147
|
+
this.registry.ensureNativeListenerRunning();
|
|
148
|
+
}
|
|
149
|
+
stopListening() {
|
|
150
|
+
const entry = this.getEntry();
|
|
151
|
+
entry.started = false;
|
|
152
|
+
this.registry.maybeStopNativeListener();
|
|
153
|
+
}
|
|
154
|
+
updateConfig(config) {
|
|
155
|
+
if (typeof config !== "object" || config === null) {
|
|
156
|
+
throw new TypeError("Config must be an object");
|
|
157
|
+
}
|
|
158
|
+
this.getEntry().config = config;
|
|
159
|
+
}
|
|
160
|
+
getCurrentConfig() {
|
|
161
|
+
return { ...this.getEntry().config };
|
|
162
|
+
}
|
|
163
|
+
onDeviceAdd(callback) {
|
|
164
|
+
if (typeof callback !== "function") {
|
|
165
|
+
throw new TypeError("Callback must be a function");
|
|
166
|
+
}
|
|
167
|
+
this.getEntry().addCallback = callback;
|
|
168
|
+
this.registry.ensureAddonCallbacksInstalled();
|
|
169
|
+
}
|
|
170
|
+
onDeviceRemove(callback) {
|
|
171
|
+
if (typeof callback !== "function") {
|
|
172
|
+
throw new TypeError("Callback must be a function");
|
|
173
|
+
}
|
|
174
|
+
this.getEntry().removeCallback = callback;
|
|
175
|
+
this.registry.ensureAddonCallbacksInstalled();
|
|
176
|
+
}
|
|
177
|
+
listDevices() {
|
|
178
|
+
const config = this.getEntry().config;
|
|
179
|
+
const devices = this.registry.listDevicesFromAddon();
|
|
180
|
+
return devices.filter((device) => shouldNotifyDevice(device, config)).map((device) => applyLogicalPortMapping(device, config));
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var UsbDeviceListenerRegistry = class {
|
|
184
|
+
apps = /* @__PURE__ */ new Map();
|
|
185
|
+
addon;
|
|
186
|
+
addonCallbacksInstalled = false;
|
|
187
|
+
nativeListenerRunning = false;
|
|
188
|
+
scopedListeners = /* @__PURE__ */ new Map();
|
|
189
|
+
constructor(addon) {
|
|
190
|
+
this.addon = addon ?? loadDefaultAddon();
|
|
191
|
+
}
|
|
192
|
+
getEntry(appId) {
|
|
193
|
+
return this.apps.get(appId);
|
|
194
|
+
}
|
|
195
|
+
ensureNativeListenerRunning() {
|
|
196
|
+
if (this.nativeListenerRunning) return;
|
|
197
|
+
this.nativeListenerRunning = true;
|
|
198
|
+
this.addon.startListening();
|
|
199
|
+
}
|
|
200
|
+
maybeStopNativeListener() {
|
|
201
|
+
const anyStarted = [...this.apps.values()].some((e) => e.started);
|
|
202
|
+
if (!anyStarted && this.nativeListenerRunning) {
|
|
203
|
+
this.nativeListenerRunning = false;
|
|
204
|
+
this.addon.stopListening();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
ensureAddonCallbacksInstalled() {
|
|
208
|
+
if (this.addonCallbacksInstalled) return;
|
|
209
|
+
this.addonCallbacksInstalled = true;
|
|
210
|
+
ensureAddonCallbacksInstalled(this.addon, this.apps);
|
|
211
|
+
}
|
|
212
|
+
listDevicesFromAddon() {
|
|
213
|
+
return this.addon.listDevices();
|
|
214
|
+
}
|
|
215
|
+
register(appId, config) {
|
|
216
|
+
if (typeof config !== "object" || config === null) {
|
|
217
|
+
throw new TypeError("Config must be an object");
|
|
218
|
+
}
|
|
219
|
+
const existing = this.apps.get(appId);
|
|
220
|
+
if (existing) {
|
|
221
|
+
existing.config = config;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.apps.set(appId, {
|
|
225
|
+
config: { ...config },
|
|
226
|
+
addCallback: null,
|
|
227
|
+
removeCallback: null,
|
|
228
|
+
started: false
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
getListener(appId) {
|
|
232
|
+
if (!this.apps.has(appId)) {
|
|
233
|
+
this.apps.set(appId, {
|
|
234
|
+
config: {},
|
|
235
|
+
addCallback: null,
|
|
236
|
+
removeCallback: null,
|
|
237
|
+
started: false
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
let scoped = this.scopedListeners.get(appId);
|
|
241
|
+
if (!scoped) {
|
|
242
|
+
scoped = new ScopedListener(this, appId);
|
|
243
|
+
this.scopedListeners.set(appId, scoped);
|
|
244
|
+
}
|
|
245
|
+
return scoped;
|
|
246
|
+
}
|
|
247
|
+
unregister(appId) {
|
|
248
|
+
this.scopedListeners.delete(appId);
|
|
249
|
+
const entry = this.apps.get(appId);
|
|
250
|
+
if (entry) {
|
|
251
|
+
entry.started = false;
|
|
252
|
+
entry.addCallback = null;
|
|
253
|
+
entry.removeCallback = null;
|
|
254
|
+
this.apps.delete(appId);
|
|
255
|
+
}
|
|
256
|
+
this.maybeStopNativeListener();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// src/index.ts
|
|
261
|
+
function loadDefaultAddon2() {
|
|
262
|
+
const require2 = createRequire2(import.meta.url);
|
|
263
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
264
|
+
const __dirname = path2.dirname(__filename);
|
|
265
|
+
const packageRoot = path2.join(__dirname, "..");
|
|
266
|
+
return require2("node-gyp-build")(packageRoot);
|
|
267
|
+
}
|
|
61
268
|
var UsbDeviceListener = class {
|
|
62
269
|
addon;
|
|
63
270
|
config = {};
|
|
64
271
|
userAddCallback = null;
|
|
65
272
|
userRemoveCallback = null;
|
|
66
273
|
constructor(addon) {
|
|
67
|
-
this.addon = addon ??
|
|
274
|
+
this.addon = addon ?? loadDefaultAddon2();
|
|
68
275
|
}
|
|
69
276
|
/**
|
|
70
277
|
* Start listening for USB device events
|
|
@@ -140,6 +347,8 @@ var UsbDeviceListener = class {
|
|
|
140
347
|
}
|
|
141
348
|
};
|
|
142
349
|
export {
|
|
143
|
-
|
|
350
|
+
UsbDeviceListenerRegistry,
|
|
351
|
+
UsbDeviceListener as default,
|
|
352
|
+
formatPortPath
|
|
144
353
|
};
|
|
145
354
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/device-filter.ts"],
|
|
4
|
-
"sourcesContent": ["import { createRequire } from \"node:module\";\nimport path 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\tUsbDeviceListenerI,\n} from \"./types\";\n\nexport interface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\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\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(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}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\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\tthis.addon.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\tthis.addon.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 * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\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": ";AAAA,SAAS,
|
|
6
|
-
"names": ["require"]
|
|
3
|
+
"sources": ["../src/index.ts", "../src/device-filter.ts", "../src/formatPortPath.ts", "../src/registry.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequire } from \"node:module\";\nimport path 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\tNativeAddon,\n\tUsbDeviceListenerI,\n} from \"./types\";\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\n\tprivate readonly addon: NativeAddon;\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\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\tthis.addon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tthis.addon.stopListening();\n\t}\n\n\t/**\n\t * Update the listener config at runtime.\n\t * When listening, subsequent device events and listDevices() use the new config.\n\t * When not listening, only listDevices() uses it until the next startListening().\n\t */\n\tpublic updateConfig(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}\n\n\t/**\n\t * Return a shallow copy of the current config.\n\t * Mutating the returned object does not affect the listener's internal config.\n\t */\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.config };\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\tthis.addon.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\tthis.addon.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 * Applies logical port mapping from config if set\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst devices = this.addon.listDevices();\n\t\t// Apply logical port mapping to each device\n\t\treturn devices.map((device) => applyLogicalPortMapping(device, this.config));\n\t}\n}\n\nexport { formatPortPath } from \"./formatPortPath.js\";\nexport { UsbDeviceListenerRegistry } from \"./registry.js\";\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tNativeAddon,\n\tTargetDevice,\n\tUsbDeviceListenerRegistryI,\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 * Build hub key \"${vid}-${pid}\" from device's parent hub (uppercase 4-char hex).\n */\nfunction hubKeyFromParent(vid: number, pid: number): string {\n\treturn `${vid.toString(16).toUpperCase().padStart(4, \"0\")}-${pid.toString(16).toUpperCase().padStart(4, \"0\")}`;\n}\n\n/**\n * True if device has parent hub data and is mapped in logicalPortMapByHub.\n */\nfunction isMappedInLogicalPortMapByHub(device: DeviceInfo, config: ListenerConfig): boolean {\n\tconst map = config.logicalPortMapByHub;\n\tif (!map || Object.keys(map).length === 0) return false;\n\tconst vid = device.parentHubVid;\n\tconst pid = device.parentHubPid;\n\tconst path = device.portPath;\n\tif (vid === undefined || pid === undefined || !path || path.length === 0) return false;\n\tconst key = hubKeyFromParent(vid, pid);\n\tconst portMap = map[key];\n\tif (!portMap) return false;\n\tconst physicalPort = path[path.length - 1];\n\treturn physicalPort in portMap;\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 / logicalPortMapByHub - if specified, device must be in the map (hub map takes precedence when device has hub data)\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 / logicalPortMapByHub (if specified, device must be mapped)\n\tconst hasHubMap = config.logicalPortMapByHub && Object.keys(config.logicalPortMapByHub).length > 0;\n\tconst hasLocationMap = config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0;\n\tif (\n\t\thasHubMap &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tif (!isMappedInLogicalPortMapByHub(device, config)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasLocationMap && config.logicalPortMap) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t} else if (hasHubMap) {\n\t\t// logicalPortMapByHub is set but device has no hub data - cannot be in map\n\t\treturn false;\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured.\n * logicalPortMapByHub takes precedence when device has parent hub and port path.\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\tconst mapByHub = config.logicalPortMapByHub;\n\tif (\n\t\tmapByHub &&\n\t\tObject.keys(mapByHub).length > 0 &&\n\t\tdevice.parentHubVid !== undefined &&\n\t\tdevice.parentHubPid !== undefined &&\n\t\tdevice.portPath &&\n\t\tdevice.portPath.length > 0\n\t) {\n\t\tconst key = hubKeyFromParent(device.parentHubVid, device.parentHubPid);\n\t\tconst portMap = mapByHub[key];\n\t\tconst physicalPort = device.portPath[device.portPath.length - 1];\n\t\tif (portMap && physicalPort in portMap) {\n\t\t\treturn {\n\t\t\t\t...device,\n\t\t\t\tlogicalPort: portMap[physicalPort],\n\t\t\t};\n\t\t}\n\t}\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 type { DeviceInfo } from \"./types.js\";\n\n/**\n * Format device port path for display (e.g. \"2/3/1\" for port 2, then 3, then 1).\n * Uses native portPath when available; otherwise returns locationInfo.\n *\n * @param device - Device info from the listener\n * @returns Port path string (e.g. \"2/3/1\") or locationInfo when portPath is missing\n */\nexport function formatPortPath(device: DeviceInfo): string {\n\tif (device.portPath && device.portPath.length > 0) {\n\t\treturn device.portPath.join(\"/\");\n\t}\n\treturn device.locationInfo;\n}\n", "import { createRequire } from \"node:module\";\nimport path 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\tNativeAddon,\n\tUsbDeviceListenerI,\n\tUsbDeviceListenerRegistryI,\n} from \"./types\";\n\ninterface AppEntry {\n\tconfig: ListenerConfig;\n\taddCallback: DeviceAddCallback | null;\n\tremoveCallback: DeviceRemoveCallback | null;\n\tstarted: boolean;\n}\n\nfunction loadDefaultAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\tconst __filename = fileURLToPath(import.meta.url);\n\tconst __dirname = path.dirname(__filename);\n\tconst packageRoot = path.join(__dirname, \"..\");\n\treturn require(\"node-gyp-build\")(packageRoot);\n}\n\nfunction ensureAddonCallbacksInstalled(addon: NativeAddon, apps: Map<string, AppEntry>): void {\n\t// Install once: single fan-out to all registered apps\n\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.addCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.addCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\tfor (const entry of apps.values()) {\n\t\t\tif (!entry.started || !entry.removeCallback) continue;\n\t\t\tif (shouldNotifyDevice(device, entry.config)) {\n\t\t\t\tconst mapped = applyLogicalPortMapping(device, entry.config);\n\t\t\t\tentry.removeCallback(mapped);\n\t\t\t}\n\t\t}\n\t});\n}\n\n/**\n * Scoped listener that delegates to the registry and uses one app's config.\n */\nclass ScopedListener implements UsbDeviceListenerI {\n\tconstructor(\n\t\tprivate readonly registry: UsbDeviceListenerRegistry,\n\t\tprivate readonly appId: string\n\t) {}\n\n\tprivate getEntry(): AppEntry {\n\t\tconst entry = this.registry.getEntry(this.appId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`App not registered: ${this.appId}`);\n\t\t}\n\t\treturn entry;\n\t}\n\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\tconst entry = this.getEntry();\n\t\tentry.config = config;\n\t\tentry.started = true;\n\t\tthis.registry.ensureNativeListenerRunning();\n\t}\n\n\tpublic stopListening(): void {\n\t\tconst entry = this.getEntry();\n\t\tentry.started = false;\n\t\tthis.registry.maybeStopNativeListener();\n\t}\n\n\tpublic updateConfig(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.getEntry().config = config;\n\t}\n\n\tpublic getCurrentConfig(): ListenerConfig {\n\t\treturn { ...this.getEntry().config };\n\t}\n\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.getEntry().addCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\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.getEntry().removeCallback = callback;\n\t\tthis.registry.ensureAddonCallbacksInstalled();\n\t}\n\n\tpublic listDevices(): DeviceInfo[] {\n\t\tconst config = this.getEntry().config;\n\t\tconst devices = this.registry.listDevicesFromAddon();\n\t\treturn devices\n\t\t\t.filter((device) => shouldNotifyDevice(device, config))\n\t\t\t.map((device) => applyLogicalPortMapping(device, config));\n\t}\n}\n\n/**\n * Registry for multiple application IDs, each with its own listener config.\n * One native listener is shared; events are fanned out per app based on config.\n */\nexport class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {\n\tprivate readonly apps = new Map<string, AppEntry>();\n\tprivate readonly addon: NativeAddon;\n\tprivate addonCallbacksInstalled = false;\n\tprivate nativeListenerRunning = false;\n\tprivate readonly scopedListeners = new Map<string, ScopedListener>();\n\n\tconstructor(addon?: NativeAddon) {\n\t\tthis.addon = addon ?? loadDefaultAddon();\n\t}\n\n\tgetEntry(appId: string): AppEntry | undefined {\n\t\treturn this.apps.get(appId);\n\t}\n\n\tensureNativeListenerRunning(): void {\n\t\tif (this.nativeListenerRunning) return;\n\t\tthis.nativeListenerRunning = true;\n\t\tthis.addon.startListening();\n\t}\n\n\tmaybeStopNativeListener(): void {\n\t\tconst anyStarted = [...this.apps.values()].some((e) => e.started);\n\t\tif (!anyStarted && this.nativeListenerRunning) {\n\t\t\tthis.nativeListenerRunning = false;\n\t\t\tthis.addon.stopListening();\n\t\t}\n\t}\n\n\tensureAddonCallbacksInstalled(): void {\n\t\tif (this.addonCallbacksInstalled) return;\n\t\tthis.addonCallbacksInstalled = true;\n\t\tensureAddonCallbacksInstalled(this.addon, this.apps);\n\t}\n\n\tlistDevicesFromAddon(): DeviceInfo[] {\n\t\treturn this.addon.listDevices();\n\t}\n\n\tpublic register(appId: string, 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\tconst existing = this.apps.get(appId);\n\t\tif (existing) {\n\t\t\texisting.config = config;\n\t\t\treturn;\n\t\t}\n\t\tthis.apps.set(appId, {\n\t\t\tconfig: { ...config },\n\t\t\taddCallback: null,\n\t\t\tremoveCallback: null,\n\t\t\tstarted: false,\n\t\t});\n\t}\n\n\tpublic getListener(appId: string): UsbDeviceListenerI {\n\t\tif (!this.apps.has(appId)) {\n\t\t\tthis.apps.set(appId, {\n\t\t\t\tconfig: {},\n\t\t\t\taddCallback: null,\n\t\t\t\tremoveCallback: null,\n\t\t\t\tstarted: false,\n\t\t\t});\n\t\t}\n\t\tlet scoped = this.scopedListeners.get(appId);\n\t\tif (!scoped) {\n\t\t\tscoped = new ScopedListener(this, appId);\n\t\t\tthis.scopedListeners.set(appId, scoped);\n\t\t}\n\t\treturn scoped;\n\t}\n\n\tpublic unregister(appId: string): void {\n\t\tthis.scopedListeners.delete(appId);\n\t\tconst entry = this.apps.get(appId);\n\t\tif (entry) {\n\t\t\tentry.started = false;\n\t\t\tentry.addCallback = null;\n\t\t\tentry.removeCallback = null;\n\t\t\tthis.apps.delete(appId);\n\t\t}\n\t\tthis.maybeStopNativeListener();\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createRequire", "path", "fileURLToPath", "path", "require", "loadDefaultAddon", "require", "createRequire", "fileURLToPath", "path"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, NativeAddon, UsbDeviceListenerI, UsbDeviceListenerRegistryI } from "./types";
|
|
2
|
+
interface AppEntry {
|
|
3
|
+
config: ListenerConfig;
|
|
4
|
+
addCallback: DeviceAddCallback | null;
|
|
5
|
+
removeCallback: DeviceRemoveCallback | null;
|
|
6
|
+
started: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Registry for multiple application IDs, each with its own listener config.
|
|
10
|
+
* One native listener is shared; events are fanned out per app based on config.
|
|
11
|
+
*/
|
|
12
|
+
export declare class UsbDeviceListenerRegistry implements UsbDeviceListenerRegistryI {
|
|
13
|
+
private readonly apps;
|
|
14
|
+
private readonly addon;
|
|
15
|
+
private addonCallbacksInstalled;
|
|
16
|
+
private nativeListenerRunning;
|
|
17
|
+
private readonly scopedListeners;
|
|
18
|
+
constructor(addon?: NativeAddon);
|
|
19
|
+
getEntry(appId: string): AppEntry | undefined;
|
|
20
|
+
ensureNativeListenerRunning(): void;
|
|
21
|
+
maybeStopNativeListener(): void;
|
|
22
|
+
ensureAddonCallbacksInstalled(): void;
|
|
23
|
+
listDevicesFromAddon(): DeviceInfo[];
|
|
24
|
+
register(appId: string, config: ListenerConfig): void;
|
|
25
|
+
getListener(appId: string): UsbDeviceListenerI;
|
|
26
|
+
unregister(appId: string): void;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACX,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,0BAA0B,EAC1B,MAAM,SAAS,CAAC;AAEjB,UAAU,QAAQ;IACjB,MAAM,EAAE,cAAc,CAAC;IACvB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,cAAc,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC5C,OAAO,EAAE,OAAO,CAAC;CACjB;AAqGD;;;GAGG;AACH,qBAAa,yBAA0B,YAAW,0BAA0B;IAC3E,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;gBAEzD,KAAK,CAAC,EAAE,WAAW;IAI/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS;IAI7C,2BAA2B,IAAI,IAAI;IAMnC,uBAAuB,IAAI,IAAI;IAQ/B,6BAA6B,IAAI,IAAI;IAMrC,oBAAoB,IAAI,UAAU,EAAE;IAI7B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAiBrD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB;IAiB9C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAWtC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -43,9 +43,22 @@ export interface DeviceInfo {
|
|
|
43
43
|
locationInfo: string;
|
|
44
44
|
/**
|
|
45
45
|
* User-defined logical port number from configuration
|
|
46
|
-
* null if not mapped in logicalPortMap
|
|
46
|
+
* null if not mapped in logicalPortMap or logicalPortMapByHub
|
|
47
47
|
*/
|
|
48
48
|
logicalPort: number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Port path from root to device (e.g. [2, 3, 1] means port 2, then 3, then 1).
|
|
51
|
+
* Present when the native layer provides it (e.g. Windows); undefined or empty otherwise.
|
|
52
|
+
*/
|
|
53
|
+
portPath?: number[];
|
|
54
|
+
/**
|
|
55
|
+
* VID of the hub the device is directly connected to (0 or undefined if on root).
|
|
56
|
+
*/
|
|
57
|
+
parentHubVid?: number;
|
|
58
|
+
/**
|
|
59
|
+
* PID of the hub the device is directly connected to.
|
|
60
|
+
*/
|
|
61
|
+
parentHubPid?: number;
|
|
49
62
|
}
|
|
50
63
|
/**
|
|
51
64
|
* Target device filter by VID/PID
|
|
@@ -79,6 +92,18 @@ export interface ListenerConfig {
|
|
|
79
92
|
* }
|
|
80
93
|
*/
|
|
81
94
|
logicalPortMap?: Record<string, number>;
|
|
95
|
+
/**
|
|
96
|
+
* Map physical port to logical port per hub (alternative to logicalPortMap).
|
|
97
|
+
* Key: hub identifier "${vid}-${pid}" (hex, uppercase 4-char, e.g. "1A2C-3B4D").
|
|
98
|
+
* Value: map from physical port number to logical port number.
|
|
99
|
+
* When both logicalPortMap and logicalPortMapByHub could apply, logicalPortMapByHub takes precedence.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* {
|
|
103
|
+
* "1A2C-3B4D": { 1: 4, 4: 2, 3: 3, 2: 1 }
|
|
104
|
+
* }
|
|
105
|
+
*/
|
|
106
|
+
logicalPortMapByHub?: Record<string, Record<number, number>>;
|
|
82
107
|
/**
|
|
83
108
|
* Filter to monitor only specific devices by VID/PID
|
|
84
109
|
*
|
|
@@ -121,6 +146,17 @@ export interface ListenerConfig {
|
|
|
121
146
|
* Callback type for device add events
|
|
122
147
|
*/
|
|
123
148
|
export type DeviceAddCallback = (deviceInfo: DeviceInfo) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Native addon interface (implemented by the node-gyp addon).
|
|
151
|
+
* Used for dependency injection in tests.
|
|
152
|
+
*/
|
|
153
|
+
export interface NativeAddon {
|
|
154
|
+
startListening(): void;
|
|
155
|
+
stopListening(): void;
|
|
156
|
+
onDeviceAdd(callback: DeviceAddCallback): void;
|
|
157
|
+
onDeviceRemove(callback: DeviceRemoveCallback): void;
|
|
158
|
+
listDevices(): DeviceInfo[];
|
|
159
|
+
}
|
|
124
160
|
/**
|
|
125
161
|
* Callback type for device remove events
|
|
126
162
|
*/
|
|
@@ -231,4 +267,46 @@ export interface UsbDeviceListenerI {
|
|
|
231
267
|
*/
|
|
232
268
|
listDevices(): DeviceInfo[];
|
|
233
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Registry for multiple application IDs, each with its own listener config.
|
|
272
|
+
* Use when multiple parts of the same process need separate USB device filters and callbacks.
|
|
273
|
+
*/
|
|
274
|
+
export interface UsbDeviceListenerRegistryI {
|
|
275
|
+
/**
|
|
276
|
+
* Register or update config for an application ID.
|
|
277
|
+
* Call before or after getListener(); the scoped listener's startListening(config) also updates config.
|
|
278
|
+
*
|
|
279
|
+
* @param appId - Application identifier
|
|
280
|
+
* @param config - Listener configuration for this app
|
|
281
|
+
* @throws {TypeError} If config is not an object
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* registry.register("my-app", { targetDevices: [{ vid: "04E8", pid: "6860" }] });
|
|
285
|
+
*/
|
|
286
|
+
register(appId: string, config: ListenerConfig): void;
|
|
287
|
+
/**
|
|
288
|
+
* Return a listener scoped to the given application ID.
|
|
289
|
+
* Events and listDevices() are filtered and mapped using that app's config.
|
|
290
|
+
* Creates an internal entry with empty config if the app was not yet registered.
|
|
291
|
+
*
|
|
292
|
+
* @param appId - Application identifier
|
|
293
|
+
* @returns Listener implementing UsbDeviceListenerI for this app
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* const listener = registry.getListener("my-app");
|
|
297
|
+
* listener.startListening(config);
|
|
298
|
+
* listener.onDeviceAdd((device) => { ... });
|
|
299
|
+
*/
|
|
300
|
+
getListener(appId: string): UsbDeviceListenerI;
|
|
301
|
+
/**
|
|
302
|
+
* Remove an application from the registry and stop native listener if it was the last.
|
|
303
|
+
* Safe to call multiple times.
|
|
304
|
+
*
|
|
305
|
+
* @param appId - Application identifier
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* registry.unregister("my-app");
|
|
309
|
+
*/
|
|
310
|
+
unregister(appId: string): void;
|
|
311
|
+
}
|
|
234
312
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;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;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/native/addon.cc
CHANGED
|
@@ -95,15 +95,22 @@ Napi::Value ListDevices(const Napi::CallbackInfo& info) {
|
|
|
95
95
|
|
|
96
96
|
Napi::Array result = Napi::Array::New(env, devices.size());
|
|
97
97
|
for (size_t i = 0; i < devices.size(); i++) {
|
|
98
|
+
const DeviceInfo& d = devices[i];
|
|
98
99
|
Napi::Object obj = Napi::Object::New(env);
|
|
99
|
-
obj.Set("deviceId", Napi::String::New(env,
|
|
100
|
-
obj.Set("serialNumber", Napi::String::New(env,
|
|
101
|
-
obj.Set("deviceName", Napi::String::New(env,
|
|
102
|
-
obj.Set("vid", Napi::Number::New(env,
|
|
103
|
-
obj.Set("pid", Napi::Number::New(env,
|
|
104
|
-
obj.Set("locationInfo", Napi::String::New(env,
|
|
105
|
-
// logicalPort is always null from native - it's set by JavaScript based on config
|
|
100
|
+
obj.Set("deviceId", Napi::String::New(env, d.deviceId));
|
|
101
|
+
obj.Set("serialNumber", Napi::String::New(env, d.serialNumber));
|
|
102
|
+
obj.Set("deviceName", Napi::String::New(env, d.deviceName));
|
|
103
|
+
obj.Set("vid", Napi::Number::New(env, d.vid));
|
|
104
|
+
obj.Set("pid", Napi::Number::New(env, d.pid));
|
|
105
|
+
obj.Set("locationInfo", Napi::String::New(env, d.locationInfo));
|
|
106
106
|
obj.Set("logicalPort", env.Null());
|
|
107
|
+
Napi::Array pathArr = Napi::Array::New(env, d.portPath.size());
|
|
108
|
+
for (size_t j = 0; j < d.portPath.size(); j++) {
|
|
109
|
+
pathArr[j] = Napi::Number::New(env, d.portPath[j]);
|
|
110
|
+
}
|
|
111
|
+
obj.Set("portPath", pathArr);
|
|
112
|
+
obj.Set("parentHubVid", Napi::Number::New(env, d.parentHubVid));
|
|
113
|
+
obj.Set("parentHubPid", Napi::Number::New(env, d.parentHubPid));
|
|
107
114
|
result[i] = obj;
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -21,6 +21,9 @@ struct DeviceInfo {
|
|
|
21
21
|
uint16_t pid{0}; // Product ID
|
|
22
22
|
std::string locationInfo; // Physical port location (platform-specific format)
|
|
23
23
|
int logicalPort{-1}; // User-defined logical port number (-1 if not mapped)
|
|
24
|
+
std::vector<int> portPath; // Port numbers from root to device (e.g. [2, 3, 1])
|
|
25
|
+
uint16_t parentHubVid{0}; // VID of hub device is directly connected to (0 if on root)
|
|
26
|
+
uint16_t parentHubPid{0}; // PID of that hub
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -197,6 +197,13 @@ void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
|
|
|
197
197
|
} else {
|
|
198
198
|
obj.Set("logicalPort", env.Null());
|
|
199
199
|
}
|
|
200
|
+
Napi::Array pathArr = Napi::Array::New(env, data->portPath.size());
|
|
201
|
+
for (size_t j = 0; j < data->portPath.size(); j++) {
|
|
202
|
+
pathArr[j] = Napi::Number::New(env, data->portPath[j]);
|
|
203
|
+
}
|
|
204
|
+
obj.Set("portPath", pathArr);
|
|
205
|
+
obj.Set("parentHubVid", Napi::Number::New(env, data->parentHubVid));
|
|
206
|
+
obj.Set("parentHubPid", Napi::Number::New(env, data->parentHubPid));
|
|
200
207
|
jsCallback.Call({obj});
|
|
201
208
|
delete data;
|
|
202
209
|
};
|
|
@@ -215,6 +222,13 @@ void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
|
|
|
215
222
|
} else {
|
|
216
223
|
obj.Set("logicalPort", env.Null());
|
|
217
224
|
}
|
|
225
|
+
Napi::Array pathArr = Napi::Array::New(env, data->portPath.size());
|
|
226
|
+
for (size_t j = 0; j < data->portPath.size(); j++) {
|
|
227
|
+
pathArr[j] = Napi::Number::New(env, data->portPath[j]);
|
|
228
|
+
}
|
|
229
|
+
obj.Set("portPath", pathArr);
|
|
230
|
+
obj.Set("parentHubVid", Napi::Number::New(env, data->parentHubVid));
|
|
231
|
+
obj.Set("parentHubPid", Napi::Number::New(env, data->parentHubPid));
|
|
218
232
|
jsCallback.Call({obj});
|
|
219
233
|
delete data;
|
|
220
234
|
};
|
|
@@ -325,6 +339,7 @@ bool USBListener::GetDeviceInfo(const std::string& devicePath, DeviceInfo& info)
|
|
|
325
339
|
// Get device name
|
|
326
340
|
info.deviceName = GetDeviceName(deviceInfoSet, devInfoData);
|
|
327
341
|
|
|
342
|
+
FillPortPathAndParentHub(devInst, info);
|
|
328
343
|
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
329
344
|
return true;
|
|
330
345
|
}
|
|
@@ -349,6 +364,87 @@ bool USBListener::GetLocationInfo(DEVINST devInst, std::string& locationInfo) {
|
|
|
349
364
|
return true;
|
|
350
365
|
}
|
|
351
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Parse port number from Windows location string.
|
|
369
|
+
* Supports "Port_#0005.Hub_#0002" (returns 5) and dot-separated format (last non-zero segment).
|
|
370
|
+
*/
|
|
371
|
+
bool USBListener::ParsePortFromLocation(const std::string& locationInfo, int& port) {
|
|
372
|
+
if (locationInfo.empty()) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
// Format: Port_#0005.Hub_#0002 or Port_#0005
|
|
376
|
+
size_t pos = locationInfo.find("Port_#");
|
|
377
|
+
if (pos != std::string::npos) {
|
|
378
|
+
pos += 6; // length of "Port_#"
|
|
379
|
+
size_t end = pos;
|
|
380
|
+
while (end < locationInfo.size() && (std::isxdigit(static_cast<unsigned char>(locationInfo[end])) || locationInfo[end] == '.')) {
|
|
381
|
+
if (locationInfo[end] == '.') break;
|
|
382
|
+
++end;
|
|
383
|
+
}
|
|
384
|
+
if (end > pos) {
|
|
385
|
+
try {
|
|
386
|
+
port = std::stoi(locationInfo.substr(pos, end - pos), nullptr, 10);
|
|
387
|
+
return true;
|
|
388
|
+
} catch (...) {
|
|
389
|
+
// fall through to dot-separated
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Dot-separated format (e.g. 0000.001a.0000.001.003)
|
|
394
|
+
size_t last = locationInfo.rfind('.');
|
|
395
|
+
std::string segment = (last != std::string::npos && last + 1 < locationInfo.size())
|
|
396
|
+
? locationInfo.substr(last + 1) : locationInfo;
|
|
397
|
+
if (!segment.empty()) {
|
|
398
|
+
try {
|
|
399
|
+
port = std::stoi(segment, nullptr, 10);
|
|
400
|
+
return true;
|
|
401
|
+
} catch (...) {}
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Walk device tree from device to root, collect port numbers, set portPath (root-to-device) and parent hub VID/PID.
|
|
408
|
+
*/
|
|
409
|
+
void USBListener::FillPortPathAndParentHub(DEVINST devInst, DeviceInfo& info) {
|
|
410
|
+
info.portPath.clear();
|
|
411
|
+
info.parentHubVid = 0;
|
|
412
|
+
info.parentHubPid = 0;
|
|
413
|
+
std::vector<int> pathDeviceToRoot;
|
|
414
|
+
DEVINST current = devInst;
|
|
415
|
+
bool firstParent = true;
|
|
416
|
+
for (;;) {
|
|
417
|
+
std::string location;
|
|
418
|
+
if (!GetLocationInfo(current, location)) {
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
int portNum = 0;
|
|
422
|
+
if (ParsePortFromLocation(location, portNum)) {
|
|
423
|
+
pathDeviceToRoot.push_back(portNum);
|
|
424
|
+
}
|
|
425
|
+
DEVINST parentInst = 0;
|
|
426
|
+
CONFIGRET cr = CM_Get_Parent(&parentInst, current, 0);
|
|
427
|
+
if (cr != CR_SUCCESS || parentInst == 0 || parentInst == current) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
if (firstParent) {
|
|
431
|
+
firstParent = false;
|
|
432
|
+
WCHAR deviceIdBuffer[256];
|
|
433
|
+
deviceIdBuffer[0] = 0;
|
|
434
|
+
if (CM_Get_Device_IDW(parentInst, deviceIdBuffer, 256, 0) == CR_SUCCESS) {
|
|
435
|
+
std::wstring idW = deviceIdBuffer;
|
|
436
|
+
std::string parentId(idW.begin(), idW.end());
|
|
437
|
+
GetVidPid(parentId, info.parentHubVid, info.parentHubPid);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
current = parentInst;
|
|
441
|
+
}
|
|
442
|
+
// Reverse so portPath is root-to-device
|
|
443
|
+
for (auto it = pathDeviceToRoot.rbegin(); it != pathDeviceToRoot.rend(); ++it) {
|
|
444
|
+
info.portPath.push_back(*it);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
352
448
|
bool USBListener::GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid) {
|
|
353
449
|
size_t vidPos = deviceId.find("VID_");
|
|
354
450
|
size_t pidPos = deviceId.find("PID_");
|
|
@@ -451,6 +547,7 @@ void USBListener::EnumerateConnectedDevices() {
|
|
|
451
547
|
// Get device name
|
|
452
548
|
info.deviceName = GetDeviceName(deviceInfoSet, devInfoData);
|
|
453
549
|
|
|
550
|
+
FillPortPathAndParentHub(devInst, info);
|
|
454
551
|
// Cache device with exclusive lock
|
|
455
552
|
{
|
|
456
553
|
std::unique_lock lock(m_cacheMutex);
|
|
@@ -471,6 +568,13 @@ void USBListener::EnumerateConnectedDevices() {
|
|
|
471
568
|
} else {
|
|
472
569
|
obj.Set("logicalPort", env.Null());
|
|
473
570
|
}
|
|
571
|
+
Napi::Array pathArr = Napi::Array::New(env, data->portPath.size());
|
|
572
|
+
for (size_t j = 0; j < data->portPath.size(); j++) {
|
|
573
|
+
pathArr[j] = Napi::Number::New(env, data->portPath[j]);
|
|
574
|
+
}
|
|
575
|
+
obj.Set("portPath", pathArr);
|
|
576
|
+
obj.Set("parentHubVid", Napi::Number::New(env, data->parentHubVid));
|
|
577
|
+
obj.Set("parentHubPid", Napi::Number::New(env, data->parentHubPid));
|
|
474
578
|
jsCallback.Call({obj});
|
|
475
579
|
delete data;
|
|
476
580
|
};
|
|
@@ -520,6 +624,7 @@ std::vector<DeviceInfo> USBListener::ListAllDevices() {
|
|
|
520
624
|
// Get device name
|
|
521
625
|
info.deviceName = GetDeviceName(deviceInfoSet, devInfoData);
|
|
522
626
|
|
|
627
|
+
FillPortPathAndParentHub(devInst, info);
|
|
523
628
|
devices.push_back(info);
|
|
524
629
|
}
|
|
525
630
|
}
|
|
@@ -74,6 +74,8 @@ private:
|
|
|
74
74
|
bool GetDeviceInfo(const std::string& devicePath, DeviceInfo& info); // Get device info from Windows API (for connections)
|
|
75
75
|
bool GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info); // Parse device info from path (for disconnections)
|
|
76
76
|
bool GetLocationInfo(DEVINST devInst, std::string& locationInfo); // Get physical port location
|
|
77
|
+
bool ParsePortFromLocation(const std::string& locationInfo, int& port); // Parse port number from location string
|
|
78
|
+
void FillPortPathAndParentHub(DEVINST devInst, DeviceInfo& info); // Walk device tree, set portPath and parentHubVid/Pid
|
|
77
79
|
std::string GetSerialNumber(HDEVINFO deviceInfoSet, SP_DEVINFO_DATA& devInfoData); // Get USB serial number
|
|
78
80
|
std::string GetDeviceName(HDEVINFO deviceInfoSet, SP_DEVINFO_DATA& devInfoData); // Get device description/name
|
|
79
81
|
bool GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid); // Parse VID/PID from device ID string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcesystems/usb-device-listener",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.79",
|
|
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",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"test": "vitest run",
|
|
84
84
|
"test:watch": "vitest",
|
|
85
85
|
"listDevices": "cross-env DEBUG=* tsx ./src/examples/list-devices.ts",
|
|
86
|
-
"example": "cross-env DEBUG=* tsx ./src/examples/example.ts"
|
|
86
|
+
"example": "cross-env DEBUG=* tsx ./src/examples/example.ts",
|
|
87
|
+
"example:registry": "cross-env DEBUG=* tsx ./src/examples/example-registry.ts"
|
|
87
88
|
}
|
|
88
89
|
}
|
|
Binary file
|