Cross-platform Node.js library for USB device communication with both legacy and WebUSB-compatible APIs.
—
Event handling provides methods to monitor USB device attach/detach events and handle hotplug scenarios. This includes automatic device discovery, event-driven programming, and robust handling of device connection changes.
Monitor USB device attach and detach events.
/**
* USB device events interface
*/
interface DeviceEvents {
/** Device attached event - passes Device object */
attach: Device;
/** Device detached event - passes Device object */
detach: Device;
/** Device IDs attached event - passes vendor/product ID object */
attachIds: { idVendor: number; idProduct: number };
/** Device IDs detached event - passes vendor/product ID object */
detachIds: { idVendor: number; idProduct: number };
/** New event listener added */
newListener: keyof DeviceEvents;
/** Event listener removed */
removeListener: keyof DeviceEvents;
}
/**
* USB module extends EventEmitter for device events
*/
interface USB extends EventEmitter {
/**
* Add event listener for device events
* @param event - Event name
* @param listener - Event handler function
*/
on<K extends keyof DeviceEvents>(event: K, listener: (arg: DeviceEvents[K]) => void): void;
/**
* Remove event listener
* @param event - Event name
* @param listener - Event handler function
*/
off<K extends keyof DeviceEvents>(event: K, listener: (arg: DeviceEvents[K]) => void): void;
/**
* Add one-time event listener
* @param event - Event name
* @param listener - Event handler function
*/
once<K extends keyof DeviceEvents>(event: K, listener: (arg: DeviceEvents[K]) => void): void;
}Usage Examples:
import { usb } from 'usb';
// Listen for device attach events
usb.on('attach', (device) => {
console.log('Device attached:');
console.log(` Vendor ID: 0x${device.deviceDescriptor.idVendor.toString(16).padStart(4, '0')}`);
console.log(` Product ID: 0x${device.deviceDescriptor.idProduct.toString(16).padStart(4, '0')}`);
console.log(` Bus: ${device.busNumber}, Address: ${device.deviceAddress}`);
// You can immediately use the device
try {
device.open();
console.log('Device opened successfully');
// Get device strings if available
if (device.deviceDescriptor.iManufacturer > 0) {
device.getStringDescriptor(device.deviceDescriptor.iManufacturer, (error, manufacturer) => {
if (!error && manufacturer) {
console.log(` Manufacturer: ${manufacturer}`);
}
});
}
device.close();
} catch (error) {
console.log('Could not open device (may be system device)');
}
});
// Listen for device detach events
usb.on('detach', (device) => {
console.log('Device detached:');
console.log(` Vendor ID: 0x${device.deviceDescriptor.idVendor.toString(16).padStart(4, '0')}`);
console.log(` Product ID: 0x${device.deviceDescriptor.idProduct.toString(16).padStart(4, '0')}`);
});
console.log('Monitoring USB device events. Connect or disconnect devices to see events.');
// Remove event listeners when done
process.on('SIGINT', () => {
console.log('Stopping USB event monitoring...');
usb.removeAllListeners('attach');
usb.removeAllListeners('detach');
process.exit(0);
});Configure hotplug detection behavior and polling.
/**
* USB module hotplug properties
*/
interface USB {
/** Force polling loop for hotplug events */
pollHotplug: boolean;
/** Hotplug polling loop delay in milliseconds (default: 500) */
pollHotplugDelay: number;
}
/**
* Reference/unreference hotplug events from event loop
*/
function refHotplugEvents(): void;
function unrefHotplugEvents(): void;Usage Examples:
import { usb, refHotplugEvents, unrefHotplugEvents } from 'usb';
// Configure polling behavior
console.log(`Current polling delay: ${usb.pollHotplugDelay}ms`);
console.log(`Force polling: ${usb.pollHotplug}`);
// Set custom polling delay (1 second)
usb.pollHotplugDelay = 1000;
console.log(`Set polling delay to ${usb.pollHotplugDelay}ms`);
// Force use of polling instead of native hotplug events
usb.pollHotplug = true;
console.log('Forced polling mode enabled');
// Set up device monitoring
usb.on('attach', (device) => {
console.log('Device connected (detected via polling)');
});
usb.on('detach', (device) => {
console.log('Device disconnected (detected via polling)');
});
// Unreference hotplug events to allow process to exit
// even when listening for attach/detach events
unrefHotplugEvents();
console.log('Hotplug events unreferenced - process can exit');
// Re-reference if you need to keep process alive
setTimeout(() => {
refHotplugEvents();
console.log('Hotplug events re-referenced');
}, 5000);
// Example of temporary monitoring that doesn't keep process alive
function monitorUSBForShortTime() {
unrefHotplugEvents(); // Allow process to exit
const deviceHandler = (device: Device) => {
console.log('Device event detected');
// Handle device...
};
usb.on('attach', deviceHandler);
// Stop monitoring after 10 seconds
setTimeout(() => {
usb.off('attach', deviceHandler);
console.log('Stopped USB monitoring');
}, 10000);
}Handle events for specific devices or device types.
Usage Examples:
import { usb } from 'usb';
// Monitor for specific vendor/product ID
const TARGET_VID = 0x1234;
const TARGET_PID = 0x5678;
usb.on('attach', (device) => {
const desc = device.deviceDescriptor;
if (desc.idVendor === TARGET_VID && desc.idProduct === TARGET_PID) {
console.log('Target device connected!');
handleTargetDevice(device);
}
});
usb.on('detach', (device) => {
const desc = device.deviceDescriptor;
if (desc.idVendor === TARGET_VID && desc.idProduct === TARGET_PID) {
console.log('Target device disconnected!');
handleTargetDeviceDisconnect(device);
}
});
function handleTargetDevice(device: Device) {
try {
device.open();
console.log('Target device opened for communication');
// Store device reference for later use
global.currentTargetDevice = device;
// Set up device communication...
} catch (error) {
console.error('Failed to open target device:', error);
}
}
function handleTargetDeviceDisconnect(device: Device) {
if (global.currentTargetDevice === device) {
console.log('Current target device disconnected');
global.currentTargetDevice = null;
// Clean up any ongoing operations
// The device will be automatically closed by the system
}
}
// Monitor for device class (e.g., HID devices)
usb.on('attach', (device) => {
const desc = device.deviceDescriptor;
if (desc.bDeviceClass === 3) { // HID class
console.log('HID device connected');
console.log(` VID:PID = ${desc.idVendor.toString(16)}:${desc.idProduct.toString(16)}`);
}
});
// Monitor multiple device types
const MONITORED_DEVICES = [
{ vid: 0x1234, pid: 0x5678, name: 'My Custom Device' },
{ vid: 0xABCD, pid: 0xEF01, name: 'Another Device' },
{ vid: 0x2341, pid: 0x0043, name: 'Arduino Uno' }
];
usb.on('attach', (device) => {
const desc = device.deviceDescriptor;
const knownDevice = MONITORED_DEVICES.find(d =>
d.vid === desc.idVendor && d.pid === desc.idProduct
);
if (knownDevice) {
console.log(`Known device connected: ${knownDevice.name}`);
console.log(` VID:PID = ${desc.idVendor.toString(16)}:${desc.idProduct.toString(16)}`);
// Handle specific device
handleKnownDevice(device, knownDevice);
}
});
function handleKnownDevice(device: Device, deviceInfo: any) {
console.log(`Setting up ${deviceInfo.name}...`);
try {
device.open();
// Device-specific setup based on deviceInfo
switch (deviceInfo.name) {
case 'Arduino Uno':
setupArduino(device);
break;
case 'My Custom Device':
setupCustomDevice(device);
break;
default:
setupGenericDevice(device);
}
} catch (error) {
console.error(`Failed to setup ${deviceInfo.name}:`, error);
}
}Handle errors and edge cases in event processing.
Usage Examples:
import { usb } from 'usb';
// Robust event handler with error handling
function createRobustDeviceHandler() {
const connectedDevices = new Map<string, Device>();
const attachHandler = (device: Device) => {
try {
const deviceKey = `${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}:${device.busNumber}:${device.deviceAddress}`;
console.log(`Device attached: ${deviceKey}`);
// Store device reference
connectedDevices.set(deviceKey, device);
// Try to get device information safely
getDeviceInfoSafely(device)
.then(info => {
console.log('Device info:', info);
})
.catch(error => {
console.warn('Could not get device info:', error.message);
});
} catch (error) {
console.error('Error in attach handler:', error);
}
};
const detachHandler = (device: Device) => {
try {
const deviceKey = `${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}:${device.busNumber}:${device.deviceAddress}`;
console.log(`Device detached: ${deviceKey}`);
// Remove from tracking
connectedDevices.delete(deviceKey);
// Clean up any resources associated with this device
cleanupDeviceResources(deviceKey);
} catch (error) {
console.error('Error in detach handler:', error);
}
};
// Add error handling for the event system itself
const errorHandler = (error: Error) => {
console.error('USB event system error:', error);
// Optionally restart event monitoring
restartEventMonitoring();
};
usb.on('attach', attachHandler);
usb.on('detach', detachHandler);
usb.on('error', errorHandler);
return {
stop: () => {
usb.off('attach', attachHandler);
usb.off('detach', detachHandler);
usb.off('error', errorHandler);
connectedDevices.clear();
},
getConnectedDevices: () => Array.from(connectedDevices.values())
};
}
async function getDeviceInfoSafely(device: Device): Promise<any> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout getting device info'));
}, 5000);
try {
device.open();
const info: any = {
vid: device.deviceDescriptor.idVendor,
pid: device.deviceDescriptor.idProduct,
manufacturer: null,
product: null,
serial: null
};
let pendingStrings = 0;
let completedStrings = 0;
const checkComplete = () => {
completedStrings++;
if (completedStrings >= pendingStrings) {
clearTimeout(timeout);
device.close();
resolve(info);
}
};
// Get manufacturer string
if (device.deviceDescriptor.iManufacturer > 0) {
pendingStrings++;
device.getStringDescriptor(device.deviceDescriptor.iManufacturer, (error, value) => {
if (!error && value) info.manufacturer = value;
checkComplete();
});
}
// Get product string
if (device.deviceDescriptor.iProduct > 0) {
pendingStrings++;
device.getStringDescriptor(device.deviceDescriptor.iProduct, (error, value) => {
if (!error && value) info.product = value;
checkComplete();
});
}
// Get serial string
if (device.deviceDescriptor.iSerialNumber > 0) {
pendingStrings++;
device.getStringDescriptor(device.deviceDescriptor.iSerialNumber, (error, value) => {
if (!error && value) info.serial = value;
checkComplete();
});
}
// If no strings to fetch, resolve immediately
if (pendingStrings === 0) {
clearTimeout(timeout);
device.close();
resolve(info);
}
} catch (error) {
clearTimeout(timeout);
try { device.close(); } catch {}
reject(error);
}
});
}
function cleanupDeviceResources(deviceKey: string) {
console.log(`Cleaning up resources for device: ${deviceKey}`);
// Clean up any polling, transfers, or other resources
}
function restartEventMonitoring() {
console.log('Restarting USB event monitoring...');
// Implementation depends on your needs
}
// Usage
const monitor = createRobustDeviceHandler();
// Stop monitoring after some time
setTimeout(() => {
monitor.stop();
console.log('USB monitoring stopped');
}, 60000);Handle ID-based events when direct device objects aren't available.
/**
* Device ID events (fallback when direct device access isn't available)
*/
interface DeviceIds {
idVendor: number;
idProduct: number;
}Usage Examples:
import { usb } from 'usb';
// Listen for ID-based events (useful for systems with limited device access)
usb.on('attachIds', (deviceIds) => {
console.log('Device IDs attached:');
console.log(` Vendor ID: 0x${deviceIds.idVendor.toString(16).padStart(4, '0')}`);
console.log(` Product ID: 0x${deviceIds.idProduct.toString(16).padStart(4, '0')}`);
// Try to find the actual device
const device = findByIds(deviceIds.idVendor, deviceIds.idProduct);
if (device) {
console.log('Found corresponding device object');
handleNewDevice(device);
} else {
console.log('Device object not accessible, using IDs only');
handleDeviceByIds(deviceIds);
}
});
usb.on('detachIds', (deviceIds) => {
console.log('Device IDs detached:');
console.log(` Vendor ID: 0x${deviceIds.idVendor.toString(16).padStart(4, '0')}`);
console.log(` Product ID: 0x${deviceIds.idProduct.toString(16).padStart(4, '0')}`);
handleDeviceDisconnectByIds(deviceIds);
});
function handleDeviceByIds(deviceIds: DeviceIds) {
console.log(`Handling device by IDs: ${deviceIds.idVendor}:${deviceIds.idProduct}`);
// Store device info for later use
const deviceInfo = {
vid: deviceIds.idVendor,
pid: deviceIds.idProduct,
connected: true,
lastSeen: new Date()
};
// Add to tracking
deviceRegistry.set(`${deviceIds.idVendor}:${deviceIds.idProduct}`, deviceInfo);
}
function handleDeviceDisconnectByIds(deviceIds: DeviceIds) {
const key = `${deviceIds.idVendor}:${deviceIds.idProduct}`;
const deviceInfo = deviceRegistry.get(key);
if (deviceInfo) {
deviceInfo.connected = false;
deviceInfo.lastSeen = new Date();
console.log(`Updated disconnect info for device: ${key}`);
}
}
// Device registry for tracking by IDs
const deviceRegistry = new Map<string, any>();
// Combine both event types for comprehensive monitoring
function setupComprehensiveMonitoring() {
// Direct device events (preferred)
usb.on('attach', (device) => {
console.log('Direct device attach event');
handleDirectDeviceAttach(device);
});
usb.on('detach', (device) => {
console.log('Direct device detach event');
handleDirectDeviceDetach(device);
});
// ID-based events (fallback)
usb.on('attachIds', (deviceIds) => {
console.log('ID-based attach event');
// Only handle if we didn't get a direct event
setTimeout(() => {
if (!wasHandledDirectly(deviceIds)) {
handleDeviceByIds(deviceIds);
}
}, 100);
});
usb.on('detachIds', (deviceIds) => {
console.log('ID-based detach event');
// Only handle if we didn't get a direct event
setTimeout(() => {
if (!wasHandledDirectly(deviceIds)) {
handleDeviceDisconnectByIds(deviceIds);
}
}, 100);
});
}
const recentDirectEvents = new Set<string>();
function handleDirectDeviceAttach(device: Device) {
const key = `${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`;
recentDirectEvents.add(key);
// Remove from recent events after short delay
setTimeout(() => recentDirectEvents.delete(key), 500);
// Handle device normally
console.log('Handling direct device attach');
}
function handleDirectDeviceDetach(device: Device) {
const key = `${device.deviceDescriptor.idVendor}:${device.deviceDescriptor.idProduct}`;
recentDirectEvents.add(key);
// Remove from recent events after short delay
setTimeout(() => recentDirectEvents.delete(key), 500);
// Handle device disconnect
console.log('Handling direct device detach');
}
function wasHandledDirectly(deviceIds: DeviceIds): boolean {
const key = `${deviceIds.idVendor}:${deviceIds.idProduct}`;
return recentDirectEvents.has(key);
}Install with Tessl CLI
npx tessl i tessl/npm-usb