@mcesystems/usb-device-listener 1.0.14 → 1.0.15
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/dist/device-filter.d.ts +24 -0
- package/dist/device-filter.d.ts.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +7 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +8 -10
- package/prebuilds/win32-x64/@mcesystems+usb-device-listener.node +0 -0
- package/binding.gyp +0 -41
- package/native/addon.cc +0 -118
- package/native/usb_listener.cc +0 -414
- package/native/usb_listener.h +0 -113
- package/src/device-filter.ts +0 -89
- package/src/examples/example.ts +0 -60
- package/src/examples/list-devices.ts +0 -29
- package/src/index.ts +0 -144
- package/src/types.ts +0 -212
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DeviceInfo, ListenerConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Determine if a device notification should be sent based on the configuration.
|
|
4
|
+
*
|
|
5
|
+
* Filter priority (highest to lowest):
|
|
6
|
+
* 1. ignoredDevices - if device matches, always return false
|
|
7
|
+
* 2. listenOnlyDevices - if specified, device must match at least one
|
|
8
|
+
* 3. targetDevices - if specified, device must match at least one
|
|
9
|
+
* 4. logicalPortMap - if specified, device location must be in the map
|
|
10
|
+
*
|
|
11
|
+
* @param device - The device information from the native addon
|
|
12
|
+
* @param config - The listener configuration
|
|
13
|
+
* @returns true if the device should trigger a notification, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export declare function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Apply logical port mapping to a device if configured
|
|
18
|
+
*
|
|
19
|
+
* @param device - The device information from the native addon
|
|
20
|
+
* @param config - The listener configuration
|
|
21
|
+
* @returns Device info with logicalPort set if mapped, otherwise unchanged
|
|
22
|
+
*/
|
|
23
|
+
export declare function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo;
|
|
24
|
+
//# sourceMappingURL=device-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-filter.d.ts","sourceRoot":"","sources":["../src/device-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AA4BxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CA8BtF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CAQ9F"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DeviceAddCallback, DeviceInfo, DeviceRemoveCallback, ListenerConfig, UsbDeviceListenerI } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* USB Device Listener implementation
|
|
4
|
+
* Provides a type-safe wrapper around the native C++ addon
|
|
5
|
+
*/
|
|
6
|
+
export default class UsbDeviceListener implements UsbDeviceListenerI {
|
|
7
|
+
private config;
|
|
8
|
+
private userAddCallback;
|
|
9
|
+
private userRemoveCallback;
|
|
10
|
+
/**
|
|
11
|
+
* Start listening for USB device events
|
|
12
|
+
*/
|
|
13
|
+
startListening(config: ListenerConfig): void;
|
|
14
|
+
/**
|
|
15
|
+
* Stop listening for USB device events
|
|
16
|
+
*/
|
|
17
|
+
stopListening(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Register callback for device connection events
|
|
20
|
+
*/
|
|
21
|
+
onDeviceAdd(callback: DeviceAddCallback): void;
|
|
22
|
+
/**
|
|
23
|
+
* Register callback for device disconnection events
|
|
24
|
+
*/
|
|
25
|
+
onDeviceRemove(callback: DeviceRemoveCallback): void;
|
|
26
|
+
/**
|
|
27
|
+
* List all currently connected USB devices
|
|
28
|
+
*/
|
|
29
|
+
listDevices(): DeviceInfo[];
|
|
30
|
+
}
|
|
31
|
+
export type { DeviceInfo, DeviceAddCallback, DeviceRemoveCallback, ListenerConfig, TargetDevice, } from "./types";
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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;AAoBjB;;;GAGG;AACH,MAAM,CAAC,OAAO,OAAO,iBAAkB,YAAW,kBAAkB;IACnE,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,kBAAkB,CAAqC;IAE/D;;OAEG;IACI,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAQnD;;OAEG;IACI,aAAa,IAAI,IAAI;IAI5B;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI;IAerD;;OAEG;IACI,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAe3D;;OAEG;IACI,WAAW,IAAI,UAAU,EAAE;CAGlC;AAED,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
// src/device-filter.ts
|
|
7
|
+
function toHexString(value) {
|
|
8
|
+
return value.toString(16).toUpperCase().padStart(4, "0");
|
|
9
|
+
}
|
|
10
|
+
function matchesDevice(device, target) {
|
|
11
|
+
const deviceVid = toHexString(device.vid);
|
|
12
|
+
const devicePid = toHexString(device.pid);
|
|
13
|
+
const targetVid = target.vid.toUpperCase();
|
|
14
|
+
const targetPid = target.pid.toUpperCase();
|
|
15
|
+
return deviceVid === targetVid && devicePid === targetPid;
|
|
16
|
+
}
|
|
17
|
+
function matchesAnyDevice(device, targets) {
|
|
18
|
+
return targets.some((target) => matchesDevice(device, target));
|
|
19
|
+
}
|
|
20
|
+
function shouldNotifyDevice(device, config) {
|
|
21
|
+
if (config.ignoredDevices && config.ignoredDevices.length > 0) {
|
|
22
|
+
if (matchesAnyDevice(device, config.ignoredDevices)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {
|
|
27
|
+
if (!matchesAnyDevice(device, config.listenOnlyDevices)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (config.targetDevices && config.targetDevices.length > 0) {
|
|
32
|
+
if (!matchesAnyDevice(device, config.targetDevices)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {
|
|
37
|
+
if (!(device.locationInfo in config.logicalPortMap)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function applyLogicalPortMapping(device, config) {
|
|
44
|
+
if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
|
|
45
|
+
return {
|
|
46
|
+
...device,
|
|
47
|
+
logicalPort: config.logicalPortMap[device.locationInfo]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return device;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/index.ts
|
|
54
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
55
|
+
var __dirname = path.dirname(__filename);
|
|
56
|
+
var require2 = createRequire(import.meta.url);
|
|
57
|
+
var packageRoot = path.join(__dirname, "..");
|
|
58
|
+
var addon = require2("node-gyp-build")(packageRoot);
|
|
59
|
+
var { listDevices, onDeviceAdd, onDeviceRemove, startListening, stopListening } = addon;
|
|
60
|
+
var UsbDeviceListener = class {
|
|
61
|
+
config = {};
|
|
62
|
+
userAddCallback = null;
|
|
63
|
+
userRemoveCallback = null;
|
|
64
|
+
/**
|
|
65
|
+
* Start listening for USB device events
|
|
66
|
+
*/
|
|
67
|
+
startListening(config) {
|
|
68
|
+
if (typeof config !== "object" || config === null) {
|
|
69
|
+
throw new TypeError("Config must be an object");
|
|
70
|
+
}
|
|
71
|
+
this.config = config;
|
|
72
|
+
startListening();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Stop listening for USB device events
|
|
76
|
+
*/
|
|
77
|
+
stopListening() {
|
|
78
|
+
stopListening();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Register callback for device connection events
|
|
82
|
+
*/
|
|
83
|
+
onDeviceAdd(callback) {
|
|
84
|
+
if (typeof callback !== "function") {
|
|
85
|
+
throw new TypeError("Callback must be a function");
|
|
86
|
+
}
|
|
87
|
+
this.userAddCallback = callback;
|
|
88
|
+
onDeviceAdd((device) => {
|
|
89
|
+
if (shouldNotifyDevice(device, this.config)) {
|
|
90
|
+
const enrichedDevice = applyLogicalPortMapping(device, this.config);
|
|
91
|
+
this.userAddCallback?.(enrichedDevice);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Register callback for device disconnection events
|
|
97
|
+
*/
|
|
98
|
+
onDeviceRemove(callback) {
|
|
99
|
+
if (typeof callback !== "function") {
|
|
100
|
+
throw new TypeError("Callback must be a function");
|
|
101
|
+
}
|
|
102
|
+
this.userRemoveCallback = callback;
|
|
103
|
+
onDeviceRemove((device) => {
|
|
104
|
+
if (shouldNotifyDevice(device, this.config)) {
|
|
105
|
+
const enrichedDevice = applyLogicalPortMapping(device, this.config);
|
|
106
|
+
this.userRemoveCallback?.(enrichedDevice);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* List all currently connected USB devices
|
|
112
|
+
*/
|
|
113
|
+
listDevices() {
|
|
114
|
+
return listDevices();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
export {
|
|
118
|
+
UsbDeviceListener as default
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/device-filter.ts"],
|
|
4
|
+
"sourcesContent": ["import { createRequire } from \"node:module\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\r\nimport type {\r\n\tDeviceAddCallback,\r\n\tDeviceInfo,\r\n\tDeviceRemoveCallback,\r\n\tListenerConfig,\r\n\tUsbDeviceListenerI,\r\n} from \"./types\";\r\n\r\ninterface NativeAddon {\r\n\tstartListening(): void;\r\n\tstopListening(): void;\r\n\tonDeviceAdd(callback: DeviceAddCallback): void;\r\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\r\n\tlistDevices(): DeviceInfo[];\r\n}\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\n// Load native addon using require (CJS) from ESM context\r\nconst require = createRequire(import.meta.url);\r\nconst packageRoot = path.join(__dirname, \"..\");\r\nconst addon: NativeAddon = require(\"node-gyp-build\")(packageRoot);\r\n\r\nconst { listDevices, onDeviceAdd, onDeviceRemove, startListening, stopListening } = addon;\r\n\r\n/**\r\n * USB Device Listener implementation\r\n * Provides a type-safe wrapper around the native C++ addon\r\n */\r\nexport default class UsbDeviceListener implements UsbDeviceListenerI {\r\n\tprivate config: ListenerConfig = {};\r\n\tprivate userAddCallback: DeviceAddCallback | null = null;\r\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\r\n\r\n\t/**\r\n\t * Start listening for USB device events\r\n\t */\r\n\tpublic startListening(config: ListenerConfig): void {\r\n\t\tif (typeof config !== \"object\" || config === null) {\r\n\t\t\tthrow new TypeError(\"Config must be an object\");\r\n\t\t}\r\n\t\tthis.config = config;\r\n\t\tstartListening();\r\n\t}\r\n\r\n\t/**\r\n\t * Stop listening for USB device events\r\n\t */\r\n\tpublic stopListening(): void {\r\n\t\tstopListening();\r\n\t}\r\n\r\n\t/**\r\n\t * Register callback for device connection events\r\n\t */\r\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\r\n\t\tif (typeof callback !== \"function\") {\r\n\t\t\tthrow new TypeError(\"Callback must be a function\");\r\n\t\t}\r\n\t\tthis.userAddCallback = callback;\r\n\r\n\t\t// Set up internal callback that filters devices\r\n\t\tonDeviceAdd((device: DeviceInfo) => {\r\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\r\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\r\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t/**\r\n\t * Register callback for device disconnection events\r\n\t */\r\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\r\n\t\tif (typeof callback !== \"function\") {\r\n\t\t\tthrow new TypeError(\"Callback must be a function\");\r\n\t\t}\r\n\t\tthis.userRemoveCallback = callback;\r\n\r\n\t\t// Set up internal callback that filters devices\r\n\t\tonDeviceRemove((device: DeviceInfo) => {\r\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\r\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\r\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t/**\r\n\t * List all currently connected USB devices\r\n\t */\r\n\tpublic listDevices(): DeviceInfo[] {\r\n\t\treturn listDevices();\r\n\t}\r\n}\r\n\r\nexport type {\r\n\tDeviceInfo,\r\n\tDeviceAddCallback,\r\n\tDeviceRemoveCallback,\r\n\tListenerConfig,\r\n\tTargetDevice,\r\n} from \"./types\";\r\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap - if specified, device location must be in the map\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap (if specified, device must be mapped)\n\tif (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["require"]
|
|
7
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USB Device Listener Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Type-safe interface for monitoring USB device connections and disconnections
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Device information returned by the listener
|
|
8
|
+
*/
|
|
9
|
+
export interface DeviceInfo {
|
|
10
|
+
/**
|
|
11
|
+
* Windows device instance ID
|
|
12
|
+
* Format: "USB\VID_xxxx&PID_xxxx\..."
|
|
13
|
+
*/
|
|
14
|
+
deviceId: string;
|
|
15
|
+
/**
|
|
16
|
+
* USB Vendor ID (decimal)
|
|
17
|
+
* Use .toString(16) to convert to hex string
|
|
18
|
+
*/
|
|
19
|
+
vid: number;
|
|
20
|
+
/**
|
|
21
|
+
* USB Product ID (decimal)
|
|
22
|
+
* Use .toString(16) to convert to hex string
|
|
23
|
+
*/
|
|
24
|
+
pid: number;
|
|
25
|
+
/**
|
|
26
|
+
* Physical USB port location
|
|
27
|
+
* Format: "Port_#xxxx.Hub_#yyyy"
|
|
28
|
+
* This value is stable and can be used as a key for logical port mapping
|
|
29
|
+
*/
|
|
30
|
+
locationInfo: string;
|
|
31
|
+
/**
|
|
32
|
+
* User-defined logical port number from configuration
|
|
33
|
+
* null if not mapped in logicalPortMap
|
|
34
|
+
*/
|
|
35
|
+
logicalPort: number | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Target device filter by VID/PID
|
|
39
|
+
*/
|
|
40
|
+
export interface TargetDevice {
|
|
41
|
+
/**
|
|
42
|
+
* USB Vendor ID as hex string (e.g., "04E8")
|
|
43
|
+
*/
|
|
44
|
+
vid: string;
|
|
45
|
+
/**
|
|
46
|
+
* USB Product ID as hex string (e.g., "6860")
|
|
47
|
+
*/
|
|
48
|
+
pid: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for USB device listener
|
|
52
|
+
*/
|
|
53
|
+
export interface ListenerConfig {
|
|
54
|
+
/**
|
|
55
|
+
* Map physical port locations to logical port numbers
|
|
56
|
+
*
|
|
57
|
+
* Key: Location info string (e.g., "Port_#0005.Hub_#0002")
|
|
58
|
+
* Value: Logical port number (e.g., 1, 2, 3)
|
|
59
|
+
*
|
|
60
|
+
* Use listDevices() to discover location strings for your setup
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* {
|
|
64
|
+
* "Port_#0005.Hub_#0002": 1,
|
|
65
|
+
* "Port_#0006.Hub_#0002": 2
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
logicalPortMap?: Record<string, number>;
|
|
69
|
+
/**
|
|
70
|
+
* Filter to monitor only specific devices by VID/PID
|
|
71
|
+
*
|
|
72
|
+
* Empty array or undefined = monitor all USB devices
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* [
|
|
76
|
+
* { vid: "04E8", pid: "6860" }, // Samsung device
|
|
77
|
+
* { vid: "2341", pid: "0043" } // Arduino Uno
|
|
78
|
+
* ]
|
|
79
|
+
*/
|
|
80
|
+
targetDevices?: TargetDevice[];
|
|
81
|
+
/**
|
|
82
|
+
* Devices to ignore by VID/PID
|
|
83
|
+
*
|
|
84
|
+
* Devices matching any entry will not trigger events,
|
|
85
|
+
* even if they match other filters.
|
|
86
|
+
* This takes highest priority over all other filters.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* [
|
|
90
|
+
* { vid: "1234", pid: "5678" } // Ignore this specific device
|
|
91
|
+
* ]
|
|
92
|
+
*/
|
|
93
|
+
ignoredDevices?: TargetDevice[];
|
|
94
|
+
/**
|
|
95
|
+
* Only monitor these specific devices by VID/PID
|
|
96
|
+
*
|
|
97
|
+
* Empty array or undefined = monitor all devices (subject to other filters)
|
|
98
|
+
* When set, only devices matching an entry will trigger events.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* [
|
|
102
|
+
* { vid: "04E8", pid: "6860" } // Only listen to this device
|
|
103
|
+
* ]
|
|
104
|
+
*/
|
|
105
|
+
listenOnlyDevices?: TargetDevice[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Callback type for device add events
|
|
109
|
+
*/
|
|
110
|
+
export type DeviceAddCallback = (deviceInfo: DeviceInfo) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Callback type for device remove events
|
|
113
|
+
*/
|
|
114
|
+
export type DeviceRemoveCallback = (deviceInfo: DeviceInfo) => void;
|
|
115
|
+
/**
|
|
116
|
+
* USB Device Listener Module
|
|
117
|
+
*/
|
|
118
|
+
export interface UsbDeviceListenerI {
|
|
119
|
+
/**
|
|
120
|
+
* Start listening for USB device events
|
|
121
|
+
*
|
|
122
|
+
* Creates a background thread that monitors Windows device change messages.
|
|
123
|
+
* Callbacks registered with onDeviceAdd/onDeviceRemove will be invoked
|
|
124
|
+
* when matching devices are connected or disconnected.
|
|
125
|
+
*
|
|
126
|
+
* @param config - Listener configuration
|
|
127
|
+
* @throws {TypeError} If config is not an object
|
|
128
|
+
* @throws {Error} If listener is already running
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* listener.startListening({
|
|
132
|
+
* logicalPortMap: { "Port_#0005.Hub_#0002": 1 },
|
|
133
|
+
* targetDevices: [{ vid: "04E8", pid: "6860" }]
|
|
134
|
+
* });
|
|
135
|
+
*/
|
|
136
|
+
startListening(config: ListenerConfig): void;
|
|
137
|
+
/**
|
|
138
|
+
* Stop listening for USB device events
|
|
139
|
+
*
|
|
140
|
+
* Stops the background thread and cleans up resources.
|
|
141
|
+
* Safe to call multiple times.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* listener.stopListening();
|
|
145
|
+
*/
|
|
146
|
+
stopListening(): void;
|
|
147
|
+
/**
|
|
148
|
+
* Register callback for device connection events
|
|
149
|
+
*
|
|
150
|
+
* The callback will be invoked on the Node.js main thread whenever
|
|
151
|
+
* a matching USB device is connected.
|
|
152
|
+
*
|
|
153
|
+
* @param callback - Function to call when device is connected
|
|
154
|
+
* @throws {TypeError} If callback is not a function
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* listener.onDeviceAdd((device) => {
|
|
158
|
+
* console.log(`Device ${device.vid.toString(16)}:${device.pid.toString(16)}`);
|
|
159
|
+
* console.log(`Connected to port ${device.logicalPort}`);
|
|
160
|
+
* });
|
|
161
|
+
*/
|
|
162
|
+
onDeviceAdd(callback: DeviceAddCallback): void;
|
|
163
|
+
/**
|
|
164
|
+
* Register callback for device disconnection events
|
|
165
|
+
*
|
|
166
|
+
* The callback will be invoked on the Node.js main thread whenever
|
|
167
|
+
* a matching USB device is disconnected.
|
|
168
|
+
*
|
|
169
|
+
* @param callback - Function to call when device is disconnected
|
|
170
|
+
* @throws {TypeError} If callback is not a function
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* listener.onDeviceRemove((device) => {
|
|
174
|
+
* console.log(`Port ${device.logicalPort} disconnected`);
|
|
175
|
+
* });
|
|
176
|
+
*/
|
|
177
|
+
onDeviceRemove(callback: DeviceRemoveCallback): void;
|
|
178
|
+
/**
|
|
179
|
+
* List all currently connected USB devices
|
|
180
|
+
*
|
|
181
|
+
* Returns information about all USB devices currently connected to the system.
|
|
182
|
+
* Useful for discovering device locations and initial state.
|
|
183
|
+
*
|
|
184
|
+
* @returns Array of device information objects
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* const devices = listener.listDevices();
|
|
188
|
+
* devices.forEach(device => {
|
|
189
|
+
* console.log(`Location: ${device.locationInfo}`);
|
|
190
|
+
* console.log(`VID:PID = ${device.vid.toString(16)}:${device.pid.toString(16)}`);
|
|
191
|
+
* });
|
|
192
|
+
*/
|
|
193
|
+
listDevices(): DeviceInfo[];
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAE/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE7C;;;;;;;;OAQG;IACH,aAAa,IAAI,IAAI,CAAC;IAEtB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE/C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcesystems/usb-device-listener",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Native Windows USB device listener using PnP notifications without custom drivers",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
7
|
-
"types": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/types.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"import": "./
|
|
11
|
-
"types": "./
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/types.d.ts"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
@@ -49,11 +49,9 @@
|
|
|
49
49
|
"vitest": "^2.1.8"
|
|
50
50
|
},
|
|
51
51
|
"files": [
|
|
52
|
-
"
|
|
52
|
+
"dist",
|
|
53
53
|
"prebuilds",
|
|
54
|
-
"
|
|
55
|
-
"binding.gyp",
|
|
56
|
-
"tsconfig.json",
|
|
54
|
+
"build/Release/*.node",
|
|
57
55
|
"README.md"
|
|
58
56
|
],
|
|
59
57
|
"gypfile": true,
|
|
@@ -61,7 +59,7 @@
|
|
|
61
59
|
"win32"
|
|
62
60
|
],
|
|
63
61
|
"scripts": {
|
|
64
|
-
"prebuild:gyp": "prebuildify --
|
|
62
|
+
"prebuild:gyp": "prebuildify --napi --strip --target 25.2.1",
|
|
65
63
|
"build": "tsx esbuild.config.ts && tsc --emitDeclarationOnly",
|
|
66
64
|
"rebuild": "node-gyp rebuild",
|
|
67
65
|
"install": "node-gyp-build",
|
|
Binary file
|
package/binding.gyp
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"targets": [
|
|
3
|
-
{
|
|
4
|
-
"target_name": "usb_device_listener",
|
|
5
|
-
"sources": [
|
|
6
|
-
"native/addon.cc",
|
|
7
|
-
"native/usb_listener.cc"
|
|
8
|
-
],
|
|
9
|
-
"include_dirs": [
|
|
10
|
-
"<!@(node -p \"require('node-addon-api').include\")"
|
|
11
|
-
],
|
|
12
|
-
"dependencies": [
|
|
13
|
-
"<!(node -p \"require('node-addon-api').gyp\")"
|
|
14
|
-
],
|
|
15
|
-
"defines": [
|
|
16
|
-
"NAPI_DISABLE_CPP_EXCEPTIONS",
|
|
17
|
-
"UNICODE",
|
|
18
|
-
"_UNICODE"
|
|
19
|
-
],
|
|
20
|
-
"conditions": [
|
|
21
|
-
[
|
|
22
|
-
"OS=='win'",
|
|
23
|
-
{
|
|
24
|
-
"libraries": [
|
|
25
|
-
"Setupapi.lib",
|
|
26
|
-
"Cfgmgr32.lib"
|
|
27
|
-
],
|
|
28
|
-
"msvs_settings": {
|
|
29
|
-
"VCCLCompilerTool": {
|
|
30
|
-
"ExceptionHandling": 1,
|
|
31
|
-
"AdditionalOptions": [
|
|
32
|
-
"/std:c++20"
|
|
33
|
-
]
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
]
|
|
41
|
-
}
|
package/native/addon.cc
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#include <napi.h>
|
|
2
|
-
#include "usb_listener.h"
|
|
3
|
-
#include <memory>
|
|
4
|
-
|
|
5
|
-
static std::unique_ptr<USBListener> g_listener;
|
|
6
|
-
|
|
7
|
-
Napi::Value StartListening(const Napi::CallbackInfo& info) {
|
|
8
|
-
Napi::Env env = info.Env();
|
|
9
|
-
|
|
10
|
-
if (!g_listener) {
|
|
11
|
-
g_listener = std::make_unique<USBListener>();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
std::string errorMsg;
|
|
15
|
-
if (!g_listener->StartListening(errorMsg)) {
|
|
16
|
-
Napi::Error::New(env, errorMsg).ThrowAsJavaScriptException();
|
|
17
|
-
return env.Undefined();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return env.Undefined();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
Napi::Value StopListening(const Napi::CallbackInfo& info) {
|
|
24
|
-
Napi::Env env = info.Env();
|
|
25
|
-
|
|
26
|
-
if (g_listener) {
|
|
27
|
-
g_listener->StopListening();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return env.Undefined();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
Napi::Value OnDeviceAdd(const Napi::CallbackInfo& info) {
|
|
34
|
-
Napi::Env env = info.Env();
|
|
35
|
-
|
|
36
|
-
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
37
|
-
Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
|
|
38
|
-
return env.Undefined();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
Napi::Function callback = info[0].As<Napi::Function>();
|
|
42
|
-
|
|
43
|
-
if (!g_listener) {
|
|
44
|
-
g_listener = std::make_unique<USBListener>();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
|
|
48
|
-
env,
|
|
49
|
-
callback,
|
|
50
|
-
"DeviceAddCallback",
|
|
51
|
-
0,
|
|
52
|
-
1
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
g_listener->SetAddCallback(tsfn);
|
|
56
|
-
|
|
57
|
-
return env.Undefined();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
Napi::Value OnDeviceRemove(const Napi::CallbackInfo& info) {
|
|
61
|
-
Napi::Env env = info.Env();
|
|
62
|
-
|
|
63
|
-
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
64
|
-
Napi::TypeError::New(env, "Callback function expected").ThrowAsJavaScriptException();
|
|
65
|
-
return env.Undefined();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
Napi::Function callback = info[0].As<Napi::Function>();
|
|
69
|
-
|
|
70
|
-
if (!g_listener) {
|
|
71
|
-
g_listener = std::make_unique<USBListener>();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
|
|
75
|
-
env,
|
|
76
|
-
callback,
|
|
77
|
-
"DeviceRemoveCallback",
|
|
78
|
-
0,
|
|
79
|
-
1
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
g_listener->SetRemoveCallback(tsfn);
|
|
83
|
-
|
|
84
|
-
return env.Undefined();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
Napi::Value ListDevices(const Napi::CallbackInfo& info) {
|
|
88
|
-
Napi::Env env = info.Env();
|
|
89
|
-
|
|
90
|
-
if (!g_listener) {
|
|
91
|
-
g_listener = std::make_unique<USBListener>();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
std::vector<DeviceInfo> devices = g_listener->ListAllDevices();
|
|
95
|
-
|
|
96
|
-
Napi::Array result = Napi::Array::New(env, devices.size());
|
|
97
|
-
for (size_t i = 0; i < devices.size(); i++) {
|
|
98
|
-
Napi::Object obj = Napi::Object::New(env);
|
|
99
|
-
obj.Set("deviceId", Napi::String::New(env, devices[i].deviceId));
|
|
100
|
-
obj.Set("vid", Napi::Number::New(env, devices[i].vid));
|
|
101
|
-
obj.Set("pid", Napi::Number::New(env, devices[i].pid));
|
|
102
|
-
obj.Set("locationInfo", Napi::String::New(env, devices[i].locationInfo));
|
|
103
|
-
result[i] = obj;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
110
|
-
exports.Set("startListening", Napi::Function::New(env, StartListening));
|
|
111
|
-
exports.Set("stopListening", Napi::Function::New(env, StopListening));
|
|
112
|
-
exports.Set("onDeviceAdd", Napi::Function::New(env, OnDeviceAdd));
|
|
113
|
-
exports.Set("onDeviceRemove", Napi::Function::New(env, OnDeviceRemove));
|
|
114
|
-
exports.Set("listDevices", Napi::Function::New(env, ListDevices));
|
|
115
|
-
return exports;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
NODE_API_MODULE(usb_device_listener, Init)
|