npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mcesystems/usb-device-listener

v1.0.97

Published

Native cross-platform USB device listener for Windows and macOS

Downloads

2,827

Readme

USB Device Listener

A high-performance native Node.js addon for monitoring USB device connections and disconnections on Windows and macOS. Built with N-API for stability across Node.js versions.

Features

  • Real-time monitoring - Immediate notification of USB device events via native OS APIs
  • 🎯 Device filtering - Monitor specific devices by VID/PID or hub location
  • 📍 Physical port mapping - Map USB ports to logical port numbers for consistent device identification
  • 🧵 Thread-safe - Runs in separate thread without blocking Node.js event loop
  • 💪 Production-ready - Memory-safe, handles multiple simultaneous device changes
  • 🔍 Device enumeration - List all currently connected devices
  • 🖥️ Cross-platform - Supports both Windows and macOS

Installation

npm install

Requirements

  • 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.

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 ?? "<unmapped>"
	});
}

function handleDeviceRemove(device) {
	logDataObject("Device disconnected", {
		locationInfo: device.locationInfo,
		logicalPort: device.logicalPort ?? "<unmapped>"
	});
}

// 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 ================================================================================

Application ID registry (multiple configs)

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.

When to use:

  • Multiple logical "apps" or modules in one process, each with its own ListenerConfig
  • You want each app to call onDeviceAdd / onDeviceRemove and only see devices matching its filters
  • You want listDevices() per app to return only devices that match that app's config

Example: See src/examples/example-registry.ts. Run with pnpm example:registry.

import { UsbDeviceListenerRegistry } from "@mcesystems/usb-device-listener";

const registry = new UsbDeviceListenerRegistry();
registry.register("app-samsung", { targetDevices: [{ vid: "04E8", pid: "6860" }] });
registry.register("app-arduino", { targetDevices: [{ vid: "2341", pid: "0043" }] });

const listenerSamsung = registry.getListener("app-samsung");
const listenerArduino = registry.getListener("app-arduino");

listenerSamsung.onDeviceAdd((device) => { /* only Samsung devices */ });
listenerArduino.onDeviceAdd((device) => { /* only Arduino devices */ });

listenerSamsung.startListening({ targetDevices: [{ vid: "04E8", pid: "6860" }] });
listenerArduino.startListening({ targetDevices: [{ vid: "2341", pid: "0043" }] });

// Later: unregister and stop native listener when last app unregisters
registry.unregister("app-samsung");
registry.unregister("app-arduino");

Registry API:

  • register(appId, config) — Register or update config for an application ID.
  • getListener(appId) — Return a listener (implements UsbDeviceListenerI) scoped to that app; events and listDevices() are filtered by the app's config.
  • unregister(appId) — Remove the app and stop the native listener if it was the last one started.

API Reference

startListening(config)

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)
    • logicalPortMapByHub (Object, optional): Map physical port to logical port per hub (alternative to logicalPortMap)
      • Key: Hub identifier "${vid}-${pid}" (hex, uppercase 4-char, e.g. "1A2C-3B4D")
      • Value: Object mapping physical port number to logical port number (e.g. { 1: 4, 4: 2, 3: 3, 2: 1 })
      • When both logicalPortMap and logicalPortMapByHub could apply, logicalPortMapByHub takes precedence when the device has hub data (portPath, parentHubVid, parentHubPid)
    • 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:

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
});

stopListening()

Stop monitoring and clean up resources. Safe to call multiple times.

Example:

usbListener.stopListening();

updateConfig(config)

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:

// Replace config entirely
usbListener.updateConfig({ logicalPortMap: newMap, targetDevices: [] });

// Merge with current config
usbListener.updateConfig({ ...usbListener.getCurrentConfig(), logicalPortMap: newMap });

getCurrentConfig()

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:

const current = usbListener.getCurrentConfig();
usbListener.updateConfig({
	...current,
	targetDevices: [...(current.targetDevices ?? []), { vid: "04E8", pid: "6860" }]
});

onDeviceAdd(callback)

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
      • portPath (number[]|undefined): Port path from root to device (e.g. [2, 3, 1]); present when native layer provides it (e.g. Windows)
      • parentHubVid (number|undefined): VID of hub the device is directly connected to (0 if on root)
      • parentHubPid (number|undefined): PID of that hub

Example:

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 ?? "<unmapped>"
	});
});

onDeviceRemove(callback)

Register callback for device disconnection events. Device info format same as onDeviceAdd.

Example:

import { logDataObject } from "@mcesystems/tool-debug-g4";

usbListener.onDeviceRemove((device) => {
	logDataObject("Device disconnected", {
		port: device.logicalPort ?? "<unmapped>"
	});
});

listDevices()

Get list of all currently connected USB devices.

Returns: Array of device objects (same format as callback parameter)

Example:

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

Physical Port Location

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)

Getting Location Strings

Use the included list-devices.js utility:

node list-devices.js

Windows output (when port path is available, Port path (tree) shows the chain, e.g. 2/3/1 = port 2, then 3, then 1):

Device 1:
  Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
  VID: 0x04E8
  PID: 0x6860
  Port path (tree): 2/3/1
  Location Info (mapping key): Port_#0005.Hub_#0002

Device 2:
  Device ID: USB\VID_27C6&PID_6594\UID0014C59F
  VID: 0x27C6
  PID: 0x6594
  Port path (tree): 2/7
  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. When available, use Port path (tree) and the parent hub (from list output or parentHubVid/parentHubPid) with logicalPortMapByHub.

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.

Device Filtering

By VID/PID:

targetDevices: [
	{ vid: "2341", pid: "0043" },  // Arduino Uno
	{ vid: "0483", pid: "5740" }   // STM32
]

By Hub:

listenOnlyHubs: ["Hub_#0002"]  // Only monitor this hub (Windows)
// or
ignoredHubs: ["Hub_#0001"]     // Ignore this hub

Performance & Scalability

Handling Many Devices

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

Tested Scenarios

  • Multiple rapid connect/disconnect cycles
  • Simultaneous connection of 10+ devices
  • Hub with many devices
  • Long-running processes (hours/days)

Best Practices

  1. Use device filtering when possible to reduce CPU usage:

    targetDevices: [{ vid: "04E8", pid: "6860" }]  // Better than monitoring all
  2. Keep callbacks fast - offload heavy processing:

    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:

    onDeviceAdd((device) => {
    	try {
    		processDevice(device);
    	} catch (error) {
    		console.error('Device processing failed:', error);
    	}
    });
  4. Clean shutdown:

    process.on('SIGINT', () => {
    	usbListener.stopListening();
    	// Wait briefly for cleanup
    	setTimeout(() => process.exit(0), 100);
    });

Architecture

Windows

┌─────────────────────────────────────────┐
│          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_*              │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘

macOS

┌─────────────────────────────────────────┐
│          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

Windows

Build errors:

  • Ensure Visual Studio C++ build tools are installed
  • Run from "x64 Native Tools Command Prompt"

macOS

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

General

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

License

MIT

Contributing

Issues and pull requests welcome!