Native cross-platform USB device listener for Windows and macOS
npm install @mcesystems/usb-device-listenerbash
npm install
`
$3
- Node.js: v18+ (ESM support required)
#### Windows
- OS: Windows 10/11
- Build tools: Visual Studio 2022 with C++ build tools
#### macOS
- OS: macOS 10.15 (Catalina) or later
- Build tools: Xcode Command Line Tools (xcode-select --install)
Quick Start
Examples below use @mcesystems/tool-debug-g4 for formatted logs.
`javascript
import usbListener from "usb-device-listener";
import {
logDataObject,
logErrorObject,
logHeader,
logNamespace,
setLogLevel
} from "@mcesystems/tool-debug-g4";
logNamespace("usb");
setLogLevel("debug");
// Define configuration
const config = {
logicalPortMap: {
// Windows format: "Port_#0005.Hub_#0002"
// macOS format: "Port_#14200000"
"Port_#0005.Hub_#0002": 1, // Map physical port to logical port 1
"Port_#0006.Hub_#0002": 2
},
targetDevices: [
{ vid: "04E8", pid: "6860" } // Only monitor Samsung devices
],
ignoredHubs: [],
listenOnlyHubs: []
};
function handleDeviceAdd(device) {
logDataObject("Device connected", {
locationInfo: device.locationInfo,
vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
pid: device.pid.toString(16).toUpperCase().padStart(4, "0"),
logicalPort: device.logicalPort ?? ""
});
}
function handleDeviceRemove(device) {
logDataObject("Device disconnected", {
locationInfo: device.locationInfo,
logicalPort: device.logicalPort ?? ""
});
}
// Register event handlers
usbListener.onDeviceAdd(handleDeviceAdd);
usbListener.onDeviceRemove(handleDeviceRemove);
// Start listening
try {
usbListener.startListening(config);
logHeader("Listening for USB events");
} catch (error) {
logErrorObject(error, "Failed to start");
}
// Graceful shutdown
process.on('SIGINT', () => {
logHeader("Stopping USB listener");
usbListener.stopListening();
process.exit(0);
});
`
Example output:
`
usb:info ================================================================================
usb:info Listening for USB events
usb:info ================================================================================
usb:detail ================================================================================
usb:detail Device connected handleDeviceAdd
usb:detail ================================================================================
usb:detail
usb:detail # locationInfo: Port_#0005.Hub_#0002
usb:detail # vid: 04E8
usb:detail # pid: 6860
usb:detail # logicalPort: 1
usb:detail
usb:detail ================================================================================
`
API Reference
$3
Start monitoring USB device events.
Parameters:
- config (Object): Configuration object
- logicalPortMap (Object, optional): Map physical locations to logical port numbers
- Key: Location string (platform-specific format)
- Value: Logical port number (integer)
- targetDevices (Array, optional): Filter specific devices by VID/PID
- Each element: { vid: string, pid: string } (hex strings, e.g., "04E8")
- Empty array = monitor all devices
- ignoredHubs (Array, optional): Hub location strings to ignore
- listenOnlyHubs (Array, optional): Only monitor these hub locations
Throws:
- TypeError if config is not an object
- Error if listener is already running
Example:
`javascript
usbListener.startListening({
logicalPortMap: {
"Port_#0005.Hub_#0002": 1 // Windows
// "Port_#14200000": 1 // macOS
},
targetDevices: [], // Monitor all devices
ignoredHubs: ["Port_#0001.Hub_#0001"], // Ignore this hub
listenOnlyHubs: [] // No restriction
});
`
$3
Stop monitoring and clean up resources. Safe to call multiple times.
Example:
`javascript
usbListener.stopListening();
`
$3
Update the listener config at runtime. When listening, subsequent device events and listDevices() use the new config. When not listening, only listDevices() uses it until the next startListening(). Config is fully replaced (use getCurrentConfig() to merge).
Parameters:
- config (Object): Same shape as startListening(config) (logicalPortMap, targetDevices, ignoredDevices, listenOnlyDevices)
Throws:
- TypeError if config is not an object
Example:
`javascript
// Replace config entirely
usbListener.updateConfig({ logicalPortMap: newMap, targetDevices: [] });
// Merge with current config
usbListener.updateConfig({ ...usbListener.getCurrentConfig(), logicalPortMap: newMap });
`
$3
Return a copy of the current config. Mutating the returned object does not affect the listener's internal config. Use with updateConfig() to merge partial changes.
Returns: Shallow copy of the current config object (ListenerConfig)
Example:
`javascript
const current = usbListener.getCurrentConfig();
usbListener.updateConfig({
...current,
targetDevices: [...(current.targetDevices ?? []), { vid: "04E8", pid: "6860" }]
});
`
$3
Register callback for device connection events.
Parameters:
- callback (Function): Called when device is connected
- deviceInfo (Object):
- deviceId (string): Platform-specific device instance ID
- vid (number): Vendor ID (decimal)
- pid (number): Product ID (decimal)
- locationInfo (string): Physical port location (platform-specific format)
- logicalPort (number|null): Mapped logical port or null
Example:
`javascript
import { logDataObject } from "@mcesystems/tool-debug-g4";
usbListener.onDeviceAdd((device) => {
const vidHex = device.vid.toString(16).toUpperCase().padStart(4, "0");
const pidHex = device.pid.toString(16).toUpperCase().padStart(4, "0");
logDataObject("Device connected", {
device: ${vidHex}:${pidHex},
port: device.logicalPort ?? ""
});
});
`
$3
Register callback for device disconnection events. Device info format same as onDeviceAdd.
Example:
`javascript
import { logDataObject } from "@mcesystems/tool-debug-g4";
usbListener.onDeviceRemove((device) => {
logDataObject("Device disconnected", {
port: device.logicalPort ?? ""
});
});
`
$3
Get list of all currently connected USB devices.
Returns: Array of device objects (same format as callback parameter)
Example:
`javascript
import { logDataObject } from "@mcesystems/tool-debug-g4";
const devices = usbListener.listDevices();
devices.forEach((device) => {
logDataObject("Device", {
locationInfo: device.locationInfo,
vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
pid: device.pid.toString(16).toUpperCase().padStart(4, "0")
});
});
`
Platform-Specific Details
$3
Each platform assigns unique location strings to USB ports:
#### Windows
- Format: Port_#XXXX.Hub_#YYYY
- Example: Port_#0005.Hub_#0002
- Source: Windows Device Management API
#### macOS
- Format: Port_#XXXXXXXX (hexadecimal location ID)
- Example: Port_#14200000
- Source: IOKit locationID property (encodes bus/port path)
$3
Use the included list-devices.js utility:
`bash
node list-devices.js
`
Windows output:
`
Device 1:
Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
VID: 0x04E8
PID: 0x6860
Location Info (mapping key): Port_#0005.Hub_#0002
Device 2:
Device ID: USB\VID_27C6&PID_6594\UID0014C59F
VID: 0x27C6
PID: 0x6594
Location Info (mapping key): Port_#0007.Hub_#0002
`
macOS output:
`
Device 1:
Device ID: USB\VID_04E8&PID_6860\Port_#14200000
VID: 0x04E8
PID: 0x6860
Location Info (mapping key): Port_#14200000
Device 2:
Device ID: USB\VID_05AC&PID_8262\Port_#14100000
VID: 0x05AC
PID: 0x8262
Location Info (mapping key): Port_#14100000
`
Copy the "Location Info" values to use in your logicalPortMap.
$3
By VID/PID:
`javascript
targetDevices: [
{ vid: "2341", pid: "0043" }, // Arduino Uno
{ vid: "0483", pid: "5740" } // STM32
]
`
By Hub:
`javascript
listenOnlyHubs: ["Hub_#0002"] // Only monitor this hub (Windows)
// or
ignoredHubs: ["Hub_#0001"] // Ignore this hub
`
Performance & Scalability
$3
The listener is designed to handle multiple simultaneous device events efficiently:
β
Thread-safe: Device cache protected by mutex
β
Non-blocking: Runs in separate thread, doesn't block Node.js
β
Efficient: Only processes filtered devices
β
Memory-safe: Automatic cleanup on disconnect
$3
- Multiple rapid connect/disconnect cycles
- Simultaneous connection of 10+ devices
- Hub with many devices
- Long-running processes (hours/days)
$3
1. Use device filtering when possible to reduce CPU usage:
`javascript
targetDevices: [{ vid: "04E8", pid: "6860" }] // Better than monitoring all
`
2. Keep callbacks fast - offload heavy processing:
`javascript
onDeviceAdd((device) => {
// Good: Quick database write
db.logConnection(device);
// Bad: Long synchronous operation
// processLargeFile(device); // Use setTimeout or worker thread instead
});
`
3. Handle errors gracefully:
`javascript
onDeviceAdd((device) => {
try {
processDevice(device);
} catch (error) {
console.error('Device processing failed:', error);
}
});
`
4. Clean shutdown:
`javascript
process.on('SIGINT', () => {
usbListener.stopListening();
// Wait briefly for cleanup
setTimeout(() => process.exit(0), 100);
});
`
Architecture
$3
`
βββββββββββββββββββββββββββββββββββββββββββ
β Node.js Application β
β βββββββββββββββββββββββββββββββββββ β
β β JavaScript API β β
β β (index.js - documented) β β
β ββββββββββββββ¬βββββββββββββββββββββ β
β β β
β ββββββββββββββΌβββββββββββββββββββββ β
β β N-API Addon (addon.cc) β β
β β - Converts JS β C++ types β β
β β - ThreadSafeFunction callbacks β β
β ββββββββββββββ¬βββββββββββββββββββββ β
βββββββββββββββββΌββββββββββββββββββββββββββ
β
βββββββββββββββββΌββββββββββββββββββββββββββ
β USBListener (usb_listener_win.cc) β
β βββββββββββββββββββββββββββββββββββββ β
β β Listener Thread (MessageLoop) β β
β β - Hidden message-only window β β
β β - RegisterDeviceNotification β β
β β - Receives WM_DEVICECHANGE β β
β ββββββββββββ¬βββββββββββββββββββββββββ β
β β β
β ββββββββββββΌβββββββββββββββββββββββ β
β β Windows Device Management API β β
β β - SetupDi* functions β β
β β - CM_Get_DevNode_* β β
β ββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
`
$3
`
βββββββββββββββββββββββββββββββββββββββββββ
β Node.js Application β
β βββββββββββββββββββββββββββββββββββ β
β β JavaScript API β β
β β (index.js - documented) β β
β ββββββββββββββ¬βββββββββββββββββββββ β
β β β
β ββββββββββββββΌβββββββββββββββββββββ β
β β N-API Addon (addon.cc) β β
β β - Converts JS β C++ types β β
β β - ThreadSafeFunction callbacks β β
β ββββββββββββββ¬βββββββββββββββββββββ β
βββββββββββββββββΌββββββββββββββββββββββββββ
β
βββββββββββββββββΌββββββββββββββββββββββββββ
β USBListener (usb_listener_mac.cc) β
β βββββββββββββββββββββββββββββββββββββ β
β β Listener Thread (CFRunLoop) β β
β β - IONotificationPortRef β β
β β - kIOFirstMatchNotification β β
β β - kIOTerminatedNotification β β
β ββββββββββββ¬βββββββββββββββββββββββββ β
β β β
β ββββββββββββΌβββββββββββββββββββββββ β
β β IOKit Framework β β
β β - IOServiceMatching β β
β β - IORegistryEntryCreateCFPropertyβ β
β ββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
`
Troubleshooting
$3
Build errors:
- Ensure Visual Studio C++ build tools are installed
- Run from "x64 Native Tools Command Prompt"
$3
Build errors:
- Install Xcode Command Line Tools: xcode-select --install
- Ensure you have the latest Xcode version
Permission issues:
- USB access doesn't require special permissions on macOS
- If using App Sandbox, ensure proper entitlements
$3
No events firing:
- Check targetDevices filter isn't too restrictive
- Verify callbacks are registered before calling startListening()
- Use listDevices() to confirm devices are visible
Incorrect location info:
- Location strings are generated by the OS
- Different USB controllers may use different formats
- Always use listDevices()` to get actual location strings