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

webusb-api.mddocs/

WebUSB Compatibility

WebUSB compatibility provides a browser-compatible API that mimics the navigator.usb interface for cross-platform USB device access. This enables web applications and Node.js applications to use the same API patterns for USB communication.

Capabilities

WebUSB Interface

Main WebUSB interface compatible with browser navigator.usb.

/**
 * WebUSB Class constructor for creating custom WebUSB instances
 */
class WebUSB implements USB {
  /**
   * Create a new WebUSB instance with custom options
   * @param options - USB configuration options
   */
  constructor(options?: USBOptions);
}

/**
 * USB Options for configuring WebUSB behavior
 */
interface USBOptions {
  /** Optional device found callback for user device selection */
  devicesFound?: (devices: USBDevice[]) => Promise<USBDevice | void>;
  
  /** Optional array of preconfigured allowed devices */
  allowedDevices?: USBDeviceFilter[];
  
  /** Optional flag to automatically allow all devices */
  allowAllDevices?: boolean;
  
  /** Optional timeout in milliseconds for device control transfers */
  deviceTimeout?: number;
  
  /** Optional flag to enable/disable automatic kernel driver detaching */
  autoDetachKernelDriver?: boolean;
}

/**
 * WebUSB interface compatible with navigator.usb
 */
interface USB {
  /**
   * Request access to USB device with filters
   * @param options - Device request options with filters
   * @returns Promise resolving to selected USBDevice
   */
  requestDevice(options?: USBDeviceRequestOptions): Promise<USBDevice>;
  
  /**
   * Get previously authorized devices
   * @returns Promise resolving to array of authorized USBDevice objects
   */
  getDevices(): Promise<USBDevice[]>;
  
  /**
   * Connect event handler
   */
  onconnect: ((ev: USBConnectionEvent) => void) | undefined;
  
  /**
   * Disconnect event handler  
   */
  ondisconnect: ((ev: USBConnectionEvent) => void) | undefined;
  
  /**
   * Add event listener for connect/disconnect events
   * @param type - Event type ('connect' or 'disconnect')
   * @param listener - Event listener function
   */
  addEventListener(type: 'connect' | 'disconnect', listener: (ev: USBConnectionEvent) => void): void;
  
  /**
   * Remove event listener
   * @param type - Event type ('connect' or 'disconnect') 
   * @param callback - Event listener function to remove
   */
  removeEventListener(type: 'connect' | 'disconnect', callback: (ev: USBConnectionEvent) => void): void;
}

/**
 * Get WebUSB interface (browser navigator.usb or Node.js implementation)
 * Returns navigator.usb if available in browser, otherwise returns Node.js WebUSB instance
 * @returns USB interface
 */
function getWebUsb(): USB;

/**
 * WebUSBDevice static factory method
 */
class WebUSBDevice {
  /**
   * Create WebUSBDevice instance from legacy Device
   * @param device - Legacy USB Device object
   * @param autoDetachKernelDriver - Whether to auto-detach kernel drivers
   * @returns Promise resolving to WebUSBDevice instance
   */
  static createInstance(device: Device, autoDetachKernelDriver?: boolean): Promise<WebUSBDevice>;
}

Usage Examples:

import { webusb, getWebUsb, WebUSB } from 'usb';

// Use the default WebUSB instance
console.log('Using default WebUSB instance');

// Create custom WebUSB instance with options
const customWebUSB = new WebUSB({
  allowAllDevices: true,
  deviceTimeout: 5000,
  autoDetachKernelDriver: true,
  devicesFound: async (devices) => {
    // Custom device selection logic
    console.log(`Found ${devices.length} devices`);
    return devices.find(device => device.vendorId === 0x1234);
  }
});

// Use custom instance
const device = await customWebUSB.requestDevice({
  filters: [{ vendorId: 0x1234 }]
});

// Or get WebUSB interface (browser or Node.js)
const usb = getWebUsb();
console.log('Got WebUSB interface');

// Request device access with filters
async function requestUSBDevice() {
  try {
    const device = await webusb.requestDevice({
      filters: [
        {
          vendorId: 0x1234,
          productId: 0x5678
        },
        {
          vendorId: 0xABCD,
          classCode: 3 // HID class
        }
      ]
    });
    
    console.log('Device selected:', device.productName);
    console.log(`VID:PID = ${device.vendorId.toString(16)}:${device.productId.toString(16)}`);
    
    return device;
  } catch (error) {
    console.error('Failed to request device:', error.message);
    return null;
  }
}

// Get previously authorized devices
async function getAuthorizedDevices() {
  try {
    const devices = await webusb.getDevices();
    console.log(`Found ${devices.length} authorized devices`);
    
    devices.forEach((device, index) => {
      console.log(`Device ${index}:`);
      console.log(`  Name: ${device.productName || 'Unknown'}`);
      console.log(`  Manufacturer: ${device.manufacturerName || 'Unknown'}`);
      console.log(`  VID:PID = ${device.vendorId.toString(16)}:${device.productId.toString(16)}`);
    });
    
    return devices;
  } catch (error) {
    console.error('Failed to get devices:', error.message);
    return [];
  }
}

WebUSB Device Interface

WebUSB device interface providing browser-compatible device access.

/**
 * WebUSB Device interface
 */
interface USBDevice {
  /** USB version major number */
  readonly usbVersionMajor: number;
  
  /** USB version minor number */
  readonly usbVersionMinor: number;
  
  /** USB version subminor number */
  readonly usbVersionSubminor: number;
  
  /** Device class code */
  readonly deviceClass: number;
  
  /** Device subclass code */
  readonly deviceSubclass: number;
  
  /** Device protocol code */
  readonly deviceProtocol: number;
  
  /** Vendor ID */
  readonly vendorId: number;
  
  /** Product ID */
  readonly productId: number;
  
  /** Device version major number */
  readonly deviceVersionMajor: number;
  
  /** Device version minor number */
  readonly deviceVersionMinor: number;
  
  /** Device version subminor number */
  readonly deviceVersionSubminor: number;
  
  /** Manufacturer name string */
  readonly manufacturerName?: string;
  
  /** Product name string */
  readonly productName?: string;
  
  /** Serial number string */
  readonly serialNumber?: string;
  
  /** Array of device configurations */
  readonly configurations: USBConfiguration[];
  
  /** Current active configuration */
  readonly configuration?: USBConfiguration;
  
  /** Whether device is currently open */
  readonly opened: boolean;
}

Usage Examples:

import { webusb } from 'usb';

async function exploreUSBDevice() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234 }]
  });
  
  if (device) {
    console.log('Device Information:');
    console.log(`  USB Version: ${device.usbVersionMajor}.${device.usbVersionMinor}.${device.usbVersionSubminor}`);
    console.log(`  Device Class: ${device.deviceClass}`);
    console.log(`  Device Subclass: ${device.deviceSubclass}`);
    console.log(`  Device Protocol: ${device.deviceProtocol}`);
    console.log(`  Vendor ID: 0x${device.vendorId.toString(16).padStart(4, '0')}`);
    console.log(`  Product ID: 0x${device.productId.toString(16).padStart(4, '0')}`);
    console.log(`  Device Version: ${device.deviceVersionMajor}.${device.deviceVersionMinor}.${device.deviceVersionSubminor}`);
    
    if (device.manufacturerName) console.log(`  Manufacturer: ${device.manufacturerName}`);
    if (device.productName) console.log(`  Product: ${device.productName}`);
    if (device.serialNumber) console.log(`  Serial: ${device.serialNumber}`);
    
    console.log(`  Configurations: ${device.configurations.length}`);
    console.log(`  Currently open: ${device.opened}`);
    
    // Explore configurations
    device.configurations.forEach((config, index) => {
      console.log(`  Configuration ${index}:`);
      console.log(`    Value: ${config.configurationValue}`);
      console.log(`    Name: ${config.configurationName || 'Unnamed'}`);
      console.log(`    Interfaces: ${config.interfaces.length}`);
    });
  }
}

Device Operations

Open, close, and configure WebUSB devices.

/**
 * Device operation methods
 */
interface USBDevice {
  /**
   * Open device for communication
   * @returns Promise that resolves when device is opened
   */
  open(): Promise<void>;
  
  /**
   * Close device
   * @returns Promise that resolves when device is closed
   */
  close(): Promise<void>;
  
  /**
   * Select device configuration
   * @param configurationValue - Configuration value to select
   * @returns Promise that resolves when configuration is set
   */
  selectConfiguration(configurationValue: number): Promise<void>;
  
  /**
   * Reset the device
   * @returns Promise that resolves when device is reset
   */
  reset(): Promise<void>;
}

Usage Examples:

import { webusb } from 'usb';

async function basicDeviceOperations() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234, productId: 0x5678 }]
  });
  
  if (!device) {
    console.log('No device selected');
    return;
  }
  
  try {
    // Open device
    console.log('Opening device...');
    await device.open();
    console.log(`Device opened: ${device.opened}`);
    
    // Select configuration if needed (usually configuration 1)
    if (device.configurations.length > 1) {
      console.log('Selecting configuration 1...');
      await device.selectConfiguration(1);
      console.log('Configuration selected');
    }
    
    // Device is now ready for interface and endpoint operations
    console.log('Device ready for communication');
    
    // Perform operations...
    
  } catch (error) {
    console.error('Device operation failed:', error.message);
  } finally {
    // Always close device when done
    if (device.opened) {
      console.log('Closing device...');
      await device.close();
      console.log('Device closed');
    }
  }
}

// Device reset example
async function resetDevice() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234 }]
  });
  
  if (device) {
    try {
      await device.open();
      
      console.log('Resetting device...');
      await device.reset();
      console.log('Device reset completed');
      
      // Note: After reset, device may re-enumerate
      // You may need to close and re-request the device
      
    } catch (error) {
      console.error('Reset failed:', error.message);
    } finally {
      if (device.opened) {
        await device.close();
      }
    }
  }
}

Interface Management

Claim and manage device interfaces in WebUSB style.

/**
 * Interface management methods
 */
interface USBDevice {
  /**
   * Claim interface for exclusive access
   * @param interfaceNumber - Interface number to claim
   * @returns Promise that resolves when interface is claimed
   */
  claimInterface(interfaceNumber: number): Promise<void>;
  
  /**
   * Release previously claimed interface
   * @param interfaceNumber - Interface number to release
   * @returns Promise that resolves when interface is released
   */
  releaseInterface(interfaceNumber: number): Promise<void>;
  
  /**
   * Select alternate interface setting
   * @param interfaceNumber - Interface number
   * @param alternateSetting - Alternate setting number
   * @returns Promise that resolves when alternate setting is selected
   */
  selectAlternateInterface(interfaceNumber: number, alternateSetting: number): Promise<void>;
}

Usage Examples:

import { webusb } from 'usb';

async function manageInterfaces() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234 }]
  });
  
  if (device) {
    try {
      await device.open();
      
      // Claim interface 0
      console.log('Claiming interface 0...');
      await device.claimInterface(0);
      console.log('Interface 0 claimed');
      
      // Check if interface has alternate settings
      const config = device.configuration;
      if (config) {
        const interface0 = config.interfaces.find(iface => iface.interfaceNumber === 0);
        if (interface0 && interface0.alternates.length > 1) {
          console.log(`Interface has ${interface0.alternates.length} alternate settings`);
          
          // Select alternate setting 1
          console.log('Selecting alternate setting 1...');
          await device.selectAlternateInterface(0, 1);
          console.log('Alternate setting 1 selected');
        }
      }
      
      // Use interface for communication...
      console.log('Interface ready for use');
      
      // Release interface when done
      console.log('Releasing interface 0...');
      await device.releaseInterface(0);
      console.log('Interface 0 released');
      
    } catch (error) {
      console.error('Interface management failed:', error.message);
    } finally {
      await device.close();
    }
  }
}

// Multiple interface management
async function manageMultipleInterfaces() {
  const device = await webusb.requestDevice({
    filters: [{ classCode: 9 }] // Hub class devices often have multiple interfaces
  });
  
  if (device) {
    try {
      await device.open();
      
      const config = device.configuration;
      if (config) {
        console.log(`Device has ${config.interfaces.length} interfaces`);
        
        // Claim all available interfaces
        const claimedInterfaces: number[] = [];
        
        for (const iface of config.interfaces) {
          try {
            console.log(`Claiming interface ${iface.interfaceNumber}...`);
            await device.claimInterface(iface.interfaceNumber);
            claimedInterfaces.push(iface.interfaceNumber);
            console.log(`Interface ${iface.interfaceNumber} claimed`);
          } catch (error) {
            console.warn(`Failed to claim interface ${iface.interfaceNumber}:`, error.message);
          }
        }
        
        console.log(`Successfully claimed ${claimedInterfaces.length} interfaces`);
        
        // Use interfaces...
        
        // Release all claimed interfaces
        for (const interfaceNum of claimedInterfaces) {
          try {
            await device.releaseInterface(interfaceNum);
            console.log(`Interface ${interfaceNum} released`);
          } catch (error) {
            console.warn(`Failed to release interface ${interfaceNum}:`, error.message);
          }
        }
      }
      
    } catch (error) {
      console.error('Multiple interface management failed:', error.message);
    } finally {
      await device.close();
    }
  }
}

Data Transfers

Perform control and bulk/interrupt transfers using WebUSB API.

/**
 * Data transfer methods
 */
interface USBDevice {
  /**
   * Control transfer IN (device to host)
   * @param setup - Transfer setup parameters
   * @param length - Number of bytes to read
   * @returns Promise resolving to transfer result
   */
  controlTransferIn(setup: USBControlTransferParameters, length: number): Promise<USBInTransferResult>;
  
  /**
   * Control transfer OUT (host to device)  
   * @param setup - Transfer setup parameters
   * @param data - Data to send (optional)
   * @returns Promise resolving to transfer result
   */
  controlTransferOut(setup: USBControlTransferParameters, data?: ArrayBuffer): Promise<USBOutTransferResult>;
  
  /**
   * Bulk/Interrupt transfer IN
   * @param endpointNumber - Endpoint number (without direction bit)
   * @param length - Number of bytes to read
   * @returns Promise resolving to transfer result
   */
  transferIn(endpointNumber: number, length: number): Promise<USBInTransferResult>;
  
  /**
   * Bulk/Interrupt transfer OUT
   * @param endpointNumber - Endpoint number (without direction bit)
   * @param data - Data to send
   * @returns Promise resolving to transfer result
   */
  transferOut(endpointNumber: number, data: ArrayBuffer): Promise<USBOutTransferResult>;
  
  /**
   * Clear halt condition on endpoint
   * @param direction - Endpoint direction ('in' or 'out')
   * @param endpointNumber - Endpoint number
   * @returns Promise that resolves when halt is cleared
   */
  clearHalt(direction: USBDirection, endpointNumber: number): Promise<void>;
}

/**
 * Control transfer setup parameters
 */
interface USBControlTransferParameters {
  requestType: 'standard' | 'class' | 'vendor';
  recipient: 'device' | 'interface' | 'endpoint' | 'other';
  request: number;
  value: number;
  index: number;
}

/**
 * Transfer results
 */
interface USBInTransferResult {
  data?: DataView;
  status: 'ok' | 'stall' | 'babble';
}

interface USBOutTransferResult {
  bytesWritten: number;
  status: 'ok' | 'stall';
}

Usage Examples:

import { webusb } from 'usb';

async function performDataTransfers() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234, productId: 0x5678 }]
  });
  
  if (device) {
    try {
      await device.open();
      await device.claimInterface(0);
      
      // Control transfer IN - Get device descriptor
      console.log('Performing control transfer IN...');
      const controlResult = await device.controlTransferIn({
        requestType: 'standard',
        recipient: 'device',
        request: 0x06, // GET_DESCRIPTOR
        value: 0x0100, // Device descriptor
        index: 0x0000
      }, 18); // Device descriptor is 18 bytes
      
      if (controlResult.status === 'ok' && controlResult.data) {
        console.log(`Received ${controlResult.data.byteLength} bytes`);
        const view = controlResult.data;
        console.log(`Device descriptor bLength: ${view.getUint8(0)}`);
        console.log(`Device descriptor bDescriptorType: ${view.getUint8(1)}`);
      }
      
      // Control transfer OUT - Vendor-specific command
      console.log('Performing control transfer OUT...');
      const commandData = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
      const outResult = await device.controlTransferOut({
        requestType: 'vendor',
        recipient: 'device', 
        request: 0x01,
        value: 0x1234,
        index: 0x0000
      }, commandData.buffer);
      
      if (outResult.status === 'ok') {
        console.log(`Sent ${outResult.bytesWritten} bytes`);
      }
      
      // Bulk transfer IN
      console.log('Performing bulk transfer IN...');
      const bulkInResult = await device.transferIn(1, 64); // Endpoint 1, 64 bytes
      
      if (bulkInResult.status === 'ok' && bulkInResult.data) {
        console.log(`Bulk IN received ${bulkInResult.data.byteLength} bytes`);
        
        // Convert DataView to hex string for display
        const bytes = new Uint8Array(bulkInResult.data.buffer);
        const hexString = Array.from(bytes)
          .map(b => b.toString(16).padStart(2, '0'))
          .join(' ');
        console.log(`Data: ${hexString}`);
      }
      
      // Bulk transfer OUT
      console.log('Performing bulk transfer OUT...');
      const bulkData = new Uint8Array([0xFF, 0xAA, 0x55, 0x00]);
      const bulkOutResult = await device.transferOut(2, bulkData.buffer); // Endpoint 2
      
      if (bulkOutResult.status === 'ok') {
        console.log(`Bulk OUT sent ${bulkOutResult.bytesWritten} bytes`);
      }
      
      await device.releaseInterface(0);
      
    } catch (error) {
      console.error('Transfer failed:', error.message);
    } finally {
      await device.close();
    }
  }
}

// Handle transfer errors and retries
async function robustTransfer() {
  const device = await webusb.requestDevice({
    filters: [{ vendorId: 0x1234 }]
  });
  
  if (device) {
    try {
      await device.open();
      await device.claimInterface(0);
      
      // Robust bulk transfer with error handling
      const performBulkTransferWithRetry = async (endpointNumber: number, length: number, maxRetries: number = 3) => {
        for (let attempt = 1; attempt <= maxRetries; attempt++) {
          try {
            console.log(`Transfer attempt ${attempt}/${maxRetries}`);
            
            const result = await device.transferIn(endpointNumber, length);
            
            if (result.status === 'ok') {
              console.log('Transfer successful');
              return result.data;
            } else if (result.status === 'stall') {
              console.log('Endpoint stalled, clearing halt...');
              await device.clearHalt('in', endpointNumber);
              
              if (attempt === maxRetries) {
                throw new Error('Transfer stalled after clearing halt');
              }
            } else {
              throw new Error(`Transfer failed with status: ${result.status}`);
            }
            
          } catch (error) {
            console.error(`Attempt ${attempt} failed:`, error.message);
            
            if (attempt === maxRetries) {
              throw error;
            }
            
            // Wait before retry
            await new Promise(resolve => setTimeout(resolve, 100 * attempt));
          }
        }
      };
      
      const data = await performBulkTransferWithRetry(1, 64);
      if (data) {
        console.log(`Successfully received ${data.byteLength} bytes`);
      }
      
      await device.releaseInterface(0);
      
    } catch (error) {
      console.error('Robust transfer failed:', error.message);
    } finally {
      await device.close();
    }
  }
}

Event Handling

Handle WebUSB connect and disconnect events.

/**
 * WebUSB events
 */
interface USBConnectionEvent {
  type: 'connect' | 'disconnect';
  device: USBDevice;
}

Usage Examples:

import { webusb } from 'usb';

// Set up WebUSB event handlers
function setupWebUSBEvents() {
  // Using event handler properties
  webusb.onconnect = (event) => {
    console.log('WebUSB device connected:', event.device.productName);
    handleDeviceConnect(event.device);
  };
  
  webusb.ondisconnect = (event) => {
    console.log('WebUSB device disconnected:', event.device.productName);
    handleDeviceDisconnect(event.device);
  };
  
  // Using addEventListener (alternative approach)
  webusb.addEventListener('connect', (event) => {
    console.log('Connect event listener:', event.device.productName);
  });
  
  webusb.addEventListener('disconnect', (event) => {
    console.log('Disconnect event listener:', event.device.productName);
  });
}

function handleDeviceConnect(device: USBDevice) {
  console.log(`Device connected: ${device.productName || 'Unknown'}`);
  console.log(`  VID:PID = ${device.vendorId.toString(16)}:${device.productId.toString(16)}`);
  
  // Automatically set up device if it's a known type
  if (device.vendorId === 0x1234 && device.productId === 0x5678) {
    setupKnownDevice(device);
  }
}

function handleDeviceDisconnect(device: USBDevice) {
  console.log(`Device disconnected: ${device.productName || 'Unknown'}`);
  
  // Clean up any resources associated with this device
  cleanupDeviceResources(device);
}

async function setupKnownDevice(device: USBDevice) {
  try {
    await device.open();
    await device.claimInterface(0);
    
    console.log('Known device set up and ready for communication');
    
    // Device is ready for use
    // Store reference for later use
    global.connectedKnownDevice = device;
    
  } catch (error) {
    console.error('Failed to set up known device:', error.message);
  }
}

function cleanupDeviceResources(device: USBDevice) {
  if (global.connectedKnownDevice === device) {
    console.log('Cleaning up known device resources');
    global.connectedKnownDevice = null;
    
    // The device is already disconnected, so we don't need to close it
    // Just clean up any timers, polling, or other resources
  }
}

// Initialize event handling
setupWebUSBEvents();
console.log('WebUSB event handlers set up');

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