@mcesystems/usb-device-listener 1.0.15 → 1.0.17
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/binding.gyp +41 -0
- package/dist/index.cjs +155 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js.map +1 -1
- package/native/addon.cc +118 -0
- package/native/usb_listener.cc +414 -0
- package/native/usb_listener.h +113 -0
- package/package.json +7 -4
package/binding.gyp
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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/dist/index.cjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const __importMetaUrl = require("url").pathToFileURL(__filename).href;
|
|
2
|
+
if (typeof import.meta === "undefined") {
|
|
3
|
+
Object.defineProperty(globalThis, "import", { value: { meta: { url: __importMetaUrl } } });
|
|
4
|
+
}
|
|
5
|
+
"use strict";
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/index.ts
|
|
35
|
+
var index_exports = {};
|
|
36
|
+
__export(index_exports, {
|
|
37
|
+
default: () => UsbDeviceListener
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
var import_node_module = require("node:module");
|
|
41
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
42
|
+
var import_node_url = require("node:url");
|
|
43
|
+
|
|
44
|
+
// src/device-filter.ts
|
|
45
|
+
function toHexString(value) {
|
|
46
|
+
return value.toString(16).toUpperCase().padStart(4, "0");
|
|
47
|
+
}
|
|
48
|
+
function matchesDevice(device, target) {
|
|
49
|
+
const deviceVid = toHexString(device.vid);
|
|
50
|
+
const devicePid = toHexString(device.pid);
|
|
51
|
+
const targetVid = target.vid.toUpperCase();
|
|
52
|
+
const targetPid = target.pid.toUpperCase();
|
|
53
|
+
return deviceVid === targetVid && devicePid === targetPid;
|
|
54
|
+
}
|
|
55
|
+
function matchesAnyDevice(device, targets) {
|
|
56
|
+
return targets.some((target) => matchesDevice(device, target));
|
|
57
|
+
}
|
|
58
|
+
function shouldNotifyDevice(device, config) {
|
|
59
|
+
if (config.ignoredDevices && config.ignoredDevices.length > 0) {
|
|
60
|
+
if (matchesAnyDevice(device, config.ignoredDevices)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {
|
|
65
|
+
if (!matchesAnyDevice(device, config.listenOnlyDevices)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (config.targetDevices && config.targetDevices.length > 0) {
|
|
70
|
+
if (!matchesAnyDevice(device, config.targetDevices)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {
|
|
75
|
+
if (!(device.locationInfo in config.logicalPortMap)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function applyLogicalPortMapping(device, config) {
|
|
82
|
+
if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
|
|
83
|
+
return {
|
|
84
|
+
...device,
|
|
85
|
+
logicalPort: config.logicalPortMap[device.locationInfo]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return device;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/index.ts
|
|
92
|
+
var __filename = (0, import_node_url.fileURLToPath)(__importMetaUrl);
|
|
93
|
+
var __dirname = import_node_path.default.dirname(__filename);
|
|
94
|
+
var require2 = (0, import_node_module.createRequire)(__importMetaUrl);
|
|
95
|
+
var packageRoot = import_node_path.default.join(__dirname, "..");
|
|
96
|
+
var addon = require2("node-gyp-build")(packageRoot);
|
|
97
|
+
var { listDevices, onDeviceAdd, onDeviceRemove, startListening, stopListening } = addon;
|
|
98
|
+
var UsbDeviceListener = class {
|
|
99
|
+
config = {};
|
|
100
|
+
userAddCallback = null;
|
|
101
|
+
userRemoveCallback = null;
|
|
102
|
+
/**
|
|
103
|
+
* Start listening for USB device events
|
|
104
|
+
*/
|
|
105
|
+
startListening(config) {
|
|
106
|
+
if (typeof config !== "object" || config === null) {
|
|
107
|
+
throw new TypeError("Config must be an object");
|
|
108
|
+
}
|
|
109
|
+
this.config = config;
|
|
110
|
+
startListening();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Stop listening for USB device events
|
|
114
|
+
*/
|
|
115
|
+
stopListening() {
|
|
116
|
+
stopListening();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Register callback for device connection events
|
|
120
|
+
*/
|
|
121
|
+
onDeviceAdd(callback) {
|
|
122
|
+
if (typeof callback !== "function") {
|
|
123
|
+
throw new TypeError("Callback must be a function");
|
|
124
|
+
}
|
|
125
|
+
this.userAddCallback = callback;
|
|
126
|
+
onDeviceAdd((device) => {
|
|
127
|
+
if (shouldNotifyDevice(device, this.config)) {
|
|
128
|
+
const enrichedDevice = applyLogicalPortMapping(device, this.config);
|
|
129
|
+
this.userAddCallback?.(enrichedDevice);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Register callback for device disconnection events
|
|
135
|
+
*/
|
|
136
|
+
onDeviceRemove(callback) {
|
|
137
|
+
if (typeof callback !== "function") {
|
|
138
|
+
throw new TypeError("Callback must be a function");
|
|
139
|
+
}
|
|
140
|
+
this.userRemoveCallback = callback;
|
|
141
|
+
onDeviceRemove((device) => {
|
|
142
|
+
if (shouldNotifyDevice(device, this.config)) {
|
|
143
|
+
const enrichedDevice = applyLogicalPortMapping(device, this.config);
|
|
144
|
+
this.userRemoveCallback?.(enrichedDevice);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* List all currently connected USB devices
|
|
150
|
+
*/
|
|
151
|
+
listDevices() {
|
|
152
|
+
return listDevices();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=index.cjs.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\";\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\ninterface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Load native addon using require (CJS) from ESM context\nconst require = createRequire(import.meta.url);\nconst packageRoot = path.join(__dirname, \"..\");\nconst addon: NativeAddon = require(\"node-gyp-build\")(packageRoot);\n\nconst { listDevices, onDeviceAdd, onDeviceRemove, startListening, stopListening } = addon;\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 config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tstartListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tstopListening();\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\tonDeviceAdd((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\tonDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\treturn listDevices();\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": "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",
|
|
6
|
+
"names": ["path", "require"]
|
|
7
|
+
}
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts", "../src/device-filter.ts"],
|
|
4
|
-
"sourcesContent": ["import { createRequire } from \"node:module\";\
|
|
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\ninterface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Load native addon using require (CJS) from ESM context\nconst require = createRequire(import.meta.url);\nconst packageRoot = path.join(__dirname, \"..\");\nconst addon: NativeAddon = require(\"node-gyp-build\")(packageRoot);\n\nconst { listDevices, onDeviceAdd, onDeviceRemove, startListening, stopListening } = addon;\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 config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\tstartListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\tstopListening();\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\tonDeviceAdd((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\tonDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\treturn listDevices();\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
5
|
"mappings": "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",
|
|
6
6
|
"names": ["require"]
|
|
7
7
|
}
|
package/native/addon.cc
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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)
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#include "usb_listener.h"
|
|
2
|
+
#include <devguid.h>
|
|
3
|
+
#include <usbiodef.h> // For GUID_DEVINTERFACE_USB_DEVICE
|
|
4
|
+
#include <sstream>
|
|
5
|
+
#include <iomanip>
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
|
|
8
|
+
USBListener::USBListener() = default;
|
|
9
|
+
|
|
10
|
+
USBListener::~USBListener() {
|
|
11
|
+
StopListening();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Start the USB device listener
|
|
16
|
+
* Creates a separate thread that runs a Windows message loop to receive device notifications
|
|
17
|
+
*/
|
|
18
|
+
bool USBListener::StartListening(std::string& errorMsg) {
|
|
19
|
+
bool expected = false;
|
|
20
|
+
if (!m_running.compare_exchange_strong(expected, true)) {
|
|
21
|
+
errorMsg = "Listener already running";
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Create thread with RAII wrapper
|
|
26
|
+
HANDLE threadHandle = CreateThread(nullptr, 0, ListenerThreadProc, this, 0, nullptr);
|
|
27
|
+
if (!threadHandle) {
|
|
28
|
+
m_running = false;
|
|
29
|
+
errorMsg = "Failed to create listener thread";
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
m_thread = ThreadHandle(threadHandle);
|
|
33
|
+
|
|
34
|
+
// Brief wait for thread initialization
|
|
35
|
+
Sleep(100);
|
|
36
|
+
EnumerateConnectedDevices();
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void USBListener::StopListening() {
|
|
42
|
+
bool expected = true;
|
|
43
|
+
if (!m_running.compare_exchange_strong(expected, false)) {
|
|
44
|
+
return; // Already stopped
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Signal message loop to quit
|
|
48
|
+
if (m_hwnd) {
|
|
49
|
+
PostMessage(m_hwnd, WM_QUIT, 0, 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Thread cleanup is automatic via ThreadHandle RAII
|
|
53
|
+
// Destructor will wait and close handle
|
|
54
|
+
m_thread = ThreadHandle(); // Reset to trigger cleanup
|
|
55
|
+
|
|
56
|
+
// Release ThreadSafeFunction callbacks
|
|
57
|
+
if (m_addCallback) {
|
|
58
|
+
m_addCallback.Release();
|
|
59
|
+
}
|
|
60
|
+
if (m_removeCallback) {
|
|
61
|
+
m_removeCallback.Release();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clear device cache
|
|
65
|
+
std::unique_lock lock(m_cacheMutex);
|
|
66
|
+
m_deviceCache.clear();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void USBListener::SetAddCallback(Napi::ThreadSafeFunction callback) {
|
|
70
|
+
m_addCallback = callback;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void USBListener::SetRemoveCallback(Napi::ThreadSafeFunction callback) {
|
|
74
|
+
m_removeCallback = callback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
DWORD WINAPI USBListener::ListenerThreadProc(LPVOID param) {
|
|
78
|
+
USBListener* listener = static_cast<USBListener*>(param);
|
|
79
|
+
listener->MessageLoop();
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void USBListener::MessageLoop() {
|
|
84
|
+
WNDCLASSEX wc = {0};
|
|
85
|
+
wc.cbSize = sizeof(WNDCLASSEX);
|
|
86
|
+
wc.lpfnWndProc = WindowProc;
|
|
87
|
+
wc.hInstance = GetModuleHandle(nullptr);
|
|
88
|
+
wc.lpszClassName = L"USBListenerWindow";
|
|
89
|
+
|
|
90
|
+
if (!RegisterClassEx(&wc)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
m_hwnd = CreateWindowEx(0, L"USBListenerWindow", L"USBListener", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr);
|
|
95
|
+
if (!m_hwnd) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
SetWindowLongPtr(m_hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
|
|
100
|
+
|
|
101
|
+
DEV_BROADCAST_DEVICEINTERFACE notificationFilter = {0};
|
|
102
|
+
notificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
|
|
103
|
+
notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
|
|
104
|
+
notificationFilter.dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE;
|
|
105
|
+
|
|
106
|
+
m_notifyHandle = RegisterDeviceNotification(m_hwnd, ¬ificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
|
|
107
|
+
|
|
108
|
+
MSG msg;
|
|
109
|
+
while (m_running && GetMessage(&msg, nullptr, 0, 0)) {
|
|
110
|
+
TranslateMessage(&msg);
|
|
111
|
+
DispatchMessage(&msg);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (m_notifyHandle) {
|
|
115
|
+
UnregisterDeviceNotification(m_notifyHandle);
|
|
116
|
+
m_notifyHandle = nullptr;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (m_hwnd) {
|
|
120
|
+
DestroyWindow(m_hwnd);
|
|
121
|
+
m_hwnd = nullptr;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
UnregisterClass(L"USBListenerWindow", GetModuleHandle(nullptr));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
LRESULT CALLBACK USBListener::WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|
128
|
+
if (msg == WM_DEVICECHANGE) {
|
|
129
|
+
USBListener* listener = reinterpret_cast<USBListener*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
|
130
|
+
if (listener) {
|
|
131
|
+
listener->HandleDeviceChange(wParam, lParam);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return DefWindowProc(hwnd, msg, wParam, lParam);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle WM_DEVICECHANGE messages
|
|
139
|
+
* For connections: queries Windows API for full device info
|
|
140
|
+
* For disconnections: retrieves cached info since device is already gone
|
|
141
|
+
*/
|
|
142
|
+
void USBListener::HandleDeviceChange(WPARAM wParam, LPARAM lParam) {
|
|
143
|
+
// Only handle device arrival and removal events
|
|
144
|
+
if (wParam != DBT_DEVICEARRIVAL && wParam != DBT_DEVICEREMOVECOMPLETE) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Extract device interface information
|
|
149
|
+
DEV_BROADCAST_DEVICEINTERFACE* devInterface = reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE*>(lParam);
|
|
150
|
+
if (!devInterface || devInterface->dbcc_devicetype != DBT_DEVTYP_DEVICEINTERFACE) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Convert device path to string
|
|
155
|
+
std::wstring devicePathW = devInterface->dbcc_name;
|
|
156
|
+
std::string devicePath(devicePathW.begin(), devicePathW.end());
|
|
157
|
+
|
|
158
|
+
DeviceInfo info;
|
|
159
|
+
bool gotInfo = false;
|
|
160
|
+
|
|
161
|
+
if (wParam == DBT_DEVICEARRIVAL) {
|
|
162
|
+
// For connections, query Windows API for full device details
|
|
163
|
+
gotInfo = GetDeviceInfo(devicePath, info);
|
|
164
|
+
} else if (wParam == DBT_DEVICEREMOVECOMPLETE) {
|
|
165
|
+
// For disconnections, device is gone - retrieve from cache
|
|
166
|
+
DeviceInfo pathInfo;
|
|
167
|
+
if (GetDeviceInfoFromPath(devicePath, pathInfo)) {
|
|
168
|
+
std::unique_lock lock(m_cacheMutex); // Exclusive lock for write
|
|
169
|
+
if (auto it = m_deviceCache.find(pathInfo.deviceId); it != m_deviceCache.end()) {
|
|
170
|
+
info = std::move(it->second); // Move for efficiency
|
|
171
|
+
m_deviceCache.erase(it);
|
|
172
|
+
gotInfo = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!gotInfo) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (wParam == DBT_DEVICEARRIVAL && m_addCallback) {
|
|
182
|
+
// Cache device info for disconnect events
|
|
183
|
+
{
|
|
184
|
+
std::unique_lock lock(m_cacheMutex); // Exclusive lock for write
|
|
185
|
+
m_deviceCache[info.deviceId] = info;
|
|
186
|
+
}
|
|
187
|
+
auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
|
|
188
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
189
|
+
obj.Set("deviceId", Napi::String::New(env, data->deviceId));
|
|
190
|
+
obj.Set("vid", Napi::Number::New(env, data->vid));
|
|
191
|
+
obj.Set("pid", Napi::Number::New(env, data->pid));
|
|
192
|
+
obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
|
|
193
|
+
if (data->logicalPort >= 0) {
|
|
194
|
+
obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
|
|
195
|
+
} else {
|
|
196
|
+
obj.Set("logicalPort", env.Null());
|
|
197
|
+
}
|
|
198
|
+
jsCallback.Call({obj});
|
|
199
|
+
delete data;
|
|
200
|
+
};
|
|
201
|
+
m_addCallback.BlockingCall(new DeviceInfo(info), callback);
|
|
202
|
+
} else if (wParam == DBT_DEVICEREMOVECOMPLETE && m_removeCallback) {
|
|
203
|
+
auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
|
|
204
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
205
|
+
obj.Set("deviceId", Napi::String::New(env, data->deviceId));
|
|
206
|
+
obj.Set("vid", Napi::Number::New(env, data->vid));
|
|
207
|
+
obj.Set("pid", Napi::Number::New(env, data->pid));
|
|
208
|
+
obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
|
|
209
|
+
if (data->logicalPort >= 0) {
|
|
210
|
+
obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
|
|
211
|
+
} else {
|
|
212
|
+
obj.Set("logicalPort", env.Null());
|
|
213
|
+
}
|
|
214
|
+
jsCallback.Call({obj});
|
|
215
|
+
delete data;
|
|
216
|
+
};
|
|
217
|
+
m_removeCallback.BlockingCall(new DeviceInfo(info), callback);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
bool USBListener::GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info) {
|
|
222
|
+
size_t hashPos = devicePath.rfind('#');
|
|
223
|
+
if (hashPos == std::string::npos) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
std::string deviceIdStr = devicePath.substr(4, hashPos - 4);
|
|
228
|
+
std::replace(deviceIdStr.begin(), deviceIdStr.end(), '#', '\\');
|
|
229
|
+
|
|
230
|
+
info.deviceId = deviceIdStr;
|
|
231
|
+
|
|
232
|
+
if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
size_t lastHash = deviceIdStr.rfind('\\');
|
|
237
|
+
if (lastHash != std::string::npos) {
|
|
238
|
+
info.locationInfo = deviceIdStr.substr(lastHash + 1);
|
|
239
|
+
} else {
|
|
240
|
+
info.locationInfo = "";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
bool USBListener::GetDeviceInfo(const std::string& devicePath, DeviceInfo& info) {
|
|
247
|
+
size_t hashPos = devicePath.rfind('#');
|
|
248
|
+
if (hashPos == std::string::npos) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
std::string deviceIdStr = devicePath.substr(4, hashPos - 4);
|
|
253
|
+
std::replace(deviceIdStr.begin(), deviceIdStr.end(), '#', '\\');
|
|
254
|
+
|
|
255
|
+
std::wstring deviceIdW(deviceIdStr.begin(), deviceIdStr.end());
|
|
256
|
+
|
|
257
|
+
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
258
|
+
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
SP_DEVINFO_DATA devInfoData = {0};
|
|
263
|
+
devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
264
|
+
|
|
265
|
+
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
|
|
266
|
+
WCHAR instanceId[MAX_PATH];
|
|
267
|
+
if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
|
|
268
|
+
if (_wcsicmp(instanceId, deviceIdW.c_str()) == 0) {
|
|
269
|
+
std::wstring idW = instanceId;
|
|
270
|
+
info.deviceId = std::string(idW.begin(), idW.end());
|
|
271
|
+
|
|
272
|
+
if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
|
|
273
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
DEVINST devInst = devInfoData.DevInst;
|
|
278
|
+
if (!GetLocationInfo(devInst, info.locationInfo)) {
|
|
279
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
bool USBListener::GetLocationInfo(DEVINST devInst, std::string& locationInfo) {
|
|
294
|
+
WCHAR buffer[MAX_PATH];
|
|
295
|
+
ULONG size = sizeof(buffer);
|
|
296
|
+
|
|
297
|
+
CONFIGRET ret = CM_Get_DevNode_Registry_Property(devInst, CM_DRP_LOCATION_INFORMATION, nullptr, buffer, &size, 0);
|
|
298
|
+
if (ret != CR_SUCCESS) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
std::wstring locationW = buffer;
|
|
303
|
+
locationInfo = std::string(locationW.begin(), locationW.end());
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
bool USBListener::GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid) {
|
|
308
|
+
size_t vidPos = deviceId.find("VID_");
|
|
309
|
+
size_t pidPos = deviceId.find("PID_");
|
|
310
|
+
|
|
311
|
+
if (vidPos == std::string::npos || pidPos == std::string::npos) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
std::string vidStr = deviceId.substr(vidPos + 4, 4);
|
|
317
|
+
std::string pidStr = deviceId.substr(pidPos + 4, 4);
|
|
318
|
+
|
|
319
|
+
vid = static_cast<uint16_t>(std::stoul(vidStr, nullptr, 16));
|
|
320
|
+
pid = static_cast<uint16_t>(std::stoul(pidStr, nullptr, 16));
|
|
321
|
+
return true;
|
|
322
|
+
} catch (...) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
void USBListener::EnumerateConnectedDevices() {
|
|
328
|
+
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
329
|
+
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
SP_DEVINFO_DATA devInfoData = {0};
|
|
334
|
+
devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
335
|
+
|
|
336
|
+
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
|
|
337
|
+
WCHAR instanceId[MAX_PATH];
|
|
338
|
+
if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
|
|
339
|
+
DeviceInfo info;
|
|
340
|
+
std::wstring idW = instanceId;
|
|
341
|
+
info.deviceId = std::string(idW.begin(), idW.end());
|
|
342
|
+
|
|
343
|
+
if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
DEVINST devInst = devInfoData.DevInst;
|
|
348
|
+
if (!GetLocationInfo(devInst, info.locationInfo)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Cache device with exclusive lock
|
|
353
|
+
{
|
|
354
|
+
std::unique_lock lock(m_cacheMutex);
|
|
355
|
+
m_deviceCache[info.deviceId] = info;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (m_addCallback) {
|
|
359
|
+
auto callback = [](Napi::Env env, Napi::Function jsCallback, DeviceInfo* data) {
|
|
360
|
+
Napi::Object obj = Napi::Object::New(env);
|
|
361
|
+
obj.Set("deviceId", Napi::String::New(env, data->deviceId));
|
|
362
|
+
obj.Set("vid", Napi::Number::New(env, data->vid));
|
|
363
|
+
obj.Set("pid", Napi::Number::New(env, data->pid));
|
|
364
|
+
obj.Set("locationInfo", Napi::String::New(env, data->locationInfo));
|
|
365
|
+
if (data->logicalPort >= 0) {
|
|
366
|
+
obj.Set("logicalPort", Napi::Number::New(env, data->logicalPort));
|
|
367
|
+
} else {
|
|
368
|
+
obj.Set("logicalPort", env.Null());
|
|
369
|
+
}
|
|
370
|
+
jsCallback.Call({obj});
|
|
371
|
+
delete data;
|
|
372
|
+
};
|
|
373
|
+
m_addCallback.BlockingCall(new DeviceInfo(info), callback);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
std::vector<DeviceInfo> USBListener::ListAllDevices() {
|
|
382
|
+
std::vector<DeviceInfo> devices;
|
|
383
|
+
|
|
384
|
+
HDEVINFO deviceInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_USB_DEVICE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
|
|
385
|
+
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
|
|
386
|
+
return devices;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
SP_DEVINFO_DATA devInfoData = {0};
|
|
390
|
+
devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
|
|
391
|
+
|
|
392
|
+
for (DWORD i = 0; SetupDiEnumDeviceInfo(deviceInfoSet, i, &devInfoData); i++) {
|
|
393
|
+
WCHAR instanceId[MAX_PATH];
|
|
394
|
+
if (SetupDiGetDeviceInstanceId(deviceInfoSet, &devInfoData, instanceId, MAX_PATH, nullptr)) {
|
|
395
|
+
DeviceInfo info;
|
|
396
|
+
std::wstring idW = instanceId;
|
|
397
|
+
info.deviceId = std::string(idW.begin(), idW.end());
|
|
398
|
+
|
|
399
|
+
if (!GetVidPid(info.deviceId, info.vid, info.pid)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
DEVINST devInst = devInfoData.DevInst;
|
|
404
|
+
if (!GetLocationInfo(devInst, info.locationInfo)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
devices.push_back(info);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
|
413
|
+
return devices;
|
|
414
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#ifndef USB_LISTENER_H
|
|
2
|
+
#define USB_LISTENER_H
|
|
3
|
+
|
|
4
|
+
#include <napi.h>
|
|
5
|
+
#include <windows.h>
|
|
6
|
+
#include <initguid.h>
|
|
7
|
+
#include <setupapi.h>
|
|
8
|
+
#include <cfgmgr32.h>
|
|
9
|
+
#include <dbt.h>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <vector>
|
|
12
|
+
#include <unordered_map>
|
|
13
|
+
#include <memory>
|
|
14
|
+
#include <atomic>
|
|
15
|
+
#include <shared_mutex>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Device information structure
|
|
19
|
+
* Contains all relevant USB device identification and location data
|
|
20
|
+
*/
|
|
21
|
+
struct DeviceInfo {
|
|
22
|
+
std::string deviceId; // Windows device instance ID
|
|
23
|
+
uint16_t vid{0}; // Vendor ID
|
|
24
|
+
uint16_t pid{0}; // Product ID
|
|
25
|
+
std::string locationInfo; // Physical port location
|
|
26
|
+
int logicalPort{-1}; // User-defined logical port number (-1 if not mapped)
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* RAII wrapper for Windows thread handle
|
|
31
|
+
*/
|
|
32
|
+
class ThreadHandle {
|
|
33
|
+
public:
|
|
34
|
+
ThreadHandle() noexcept = default;
|
|
35
|
+
explicit ThreadHandle(HANDLE h) noexcept : handle(h) {}
|
|
36
|
+
~ThreadHandle() { if (handle) { WaitForSingleObject(handle, 5000); CloseHandle(handle); } }
|
|
37
|
+
|
|
38
|
+
ThreadHandle(const ThreadHandle&) = delete;
|
|
39
|
+
ThreadHandle& operator=(const ThreadHandle&) = delete;
|
|
40
|
+
ThreadHandle(ThreadHandle&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {}
|
|
41
|
+
ThreadHandle& operator=(ThreadHandle&& other) noexcept {
|
|
42
|
+
if (this != &other) {
|
|
43
|
+
if (handle) { WaitForSingleObject(handle, 5000); CloseHandle(handle); }
|
|
44
|
+
handle = std::exchange(other.handle, nullptr);
|
|
45
|
+
}
|
|
46
|
+
return *this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
HANDLE get() const noexcept { return handle; }
|
|
50
|
+
explicit operator bool() const noexcept { return handle != nullptr; }
|
|
51
|
+
|
|
52
|
+
private:
|
|
53
|
+
HANDLE handle{nullptr};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* USB Device Listener - Windows native implementation
|
|
58
|
+
*
|
|
59
|
+
* Monitors USB device connection/disconnection events using Windows WM_DEVICECHANGE messages.
|
|
60
|
+
* Runs a hidden message-only window in a separate thread to receive device notifications.
|
|
61
|
+
* Thread-safe: callbacks are invoked on the Node.js thread using Napi::ThreadSafeFunction.
|
|
62
|
+
*/
|
|
63
|
+
class USBListener {
|
|
64
|
+
public:
|
|
65
|
+
USBListener();
|
|
66
|
+
~USBListener();
|
|
67
|
+
|
|
68
|
+
// Start monitoring USB device events
|
|
69
|
+
bool StartListening(std::string& errorMsg);
|
|
70
|
+
|
|
71
|
+
// Stop monitoring and clean up resources
|
|
72
|
+
void StopListening();
|
|
73
|
+
|
|
74
|
+
// Set callback for device connection events
|
|
75
|
+
void SetAddCallback(Napi::ThreadSafeFunction callback);
|
|
76
|
+
|
|
77
|
+
// Set callback for device disconnection events
|
|
78
|
+
void SetRemoveCallback(Napi::ThreadSafeFunction callback);
|
|
79
|
+
|
|
80
|
+
// Get list of all currently connected USB devices
|
|
81
|
+
std::vector<DeviceInfo> ListAllDevices();
|
|
82
|
+
|
|
83
|
+
private:
|
|
84
|
+
// Windows message handling
|
|
85
|
+
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
|
86
|
+
static DWORD WINAPI ListenerThreadProc(LPVOID param);
|
|
87
|
+
|
|
88
|
+
// Core functionality
|
|
89
|
+
void MessageLoop(); // Main message loop in listener thread
|
|
90
|
+
void HandleDeviceChange(WPARAM wParam, LPARAM lParam); // Process WM_DEVICECHANGE messages
|
|
91
|
+
bool GetDeviceInfo(const std::string& devicePath, DeviceInfo& info); // Get device info from Windows API (for connections)
|
|
92
|
+
bool GetDeviceInfoFromPath(const std::string& devicePath, DeviceInfo& info); // Parse device info from path (for disconnections)
|
|
93
|
+
bool GetLocationInfo(DEVINST devInst, std::string& locationInfo); // Get physical port location
|
|
94
|
+
bool GetVidPid(const std::string& deviceId, uint16_t& vid, uint16_t& pid); // Parse VID/PID from device ID string
|
|
95
|
+
void EnumerateConnectedDevices(); // Enumerate devices on startup
|
|
96
|
+
|
|
97
|
+
// Windows handles (managed resources)
|
|
98
|
+
HWND m_hwnd{nullptr}; // Hidden message-only window
|
|
99
|
+
HDEVNOTIFY m_notifyHandle{nullptr}; // Device notification handle
|
|
100
|
+
ThreadHandle m_thread; // Listener thread handle (RAII)
|
|
101
|
+
std::atomic<bool> m_running{false}; // Thread control flag
|
|
102
|
+
|
|
103
|
+
// Device cache with shared_mutex for reader-writer lock pattern
|
|
104
|
+
// Multiple readers (enumerate) or single writer (add/remove)
|
|
105
|
+
mutable std::shared_mutex m_cacheMutex; // C++17 shared_mutex for better concurrency
|
|
106
|
+
std::unordered_map<std::string, DeviceInfo> m_deviceCache; // Fast lookup with hash
|
|
107
|
+
|
|
108
|
+
// Callbacks
|
|
109
|
+
Napi::ThreadSafeFunction m_addCallback; // Device add callback
|
|
110
|
+
Napi::ThreadSafeFunction m_removeCallback; // Device remove callback
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
#endif // USB_LISTENER_H
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcesystems/usb-device-listener",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Native Windows USB device listener using PnP notifications without custom drivers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/types.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/types.d.ts",
|
|
10
11
|
"import": "./dist/index.js",
|
|
11
|
-
"
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
@@ -52,14 +53,16 @@
|
|
|
52
53
|
"dist",
|
|
53
54
|
"prebuilds",
|
|
54
55
|
"build/Release/*.node",
|
|
55
|
-
"README.md"
|
|
56
|
+
"README.md",
|
|
57
|
+
"native",
|
|
58
|
+
"binding.gyp"
|
|
56
59
|
],
|
|
57
60
|
"gypfile": true,
|
|
58
61
|
"os": [
|
|
59
62
|
"win32"
|
|
60
63
|
],
|
|
61
64
|
"scripts": {
|
|
62
|
-
"prebuild:gyp": "prebuildify --napi --strip --target 25.2.1",
|
|
65
|
+
"prebuild:gyp": "prebuildify --napi --electron --strip --target 25.2.1",
|
|
63
66
|
"build": "tsx esbuild.config.ts && tsc --emitDeclarationOnly",
|
|
64
67
|
"rebuild": "node-gyp rebuild",
|
|
65
68
|
"install": "node-gyp-build",
|