CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-usb

Cross-platform Node.js library for USB device communication with both legacy and WebUSB-compatible APIs.

Pending
Overview
Eval results
Files

event-handling.mddocs/

Event Handling

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.

Capabilities

Device Events

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

Hotplug Configuration

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

Device-Specific Event Handling

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

Event Error Handling

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

ID-Based Events

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

docs

constants-errors.md

data-transfers.md

descriptors.md

device-communication.md

device-management.md

event-handling.md

index.md

interfaces-endpoints.md

webusb-api.md

tile.json