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

data-transfers.mddocs/

Data Transfers

Data transfers provide methods to perform bulk, interrupt, and control data transfers with USB endpoints. This includes single transfers, continuous polling, and stream management for efficient data communication.

Capabilities

IN Endpoint Transfers

Receive data from USB devices using IN endpoints.

/**
 * IN Endpoint for receiving data from device
 */
interface InEndpoint extends Endpoint {
  direction: 'in';
  pollActive: boolean;
  
  /**
   * Perform a transfer to read data from the endpoint
   * If length is greater than maxPacketSize, libusb will automatically split 
   * the transfer in multiple packets
   * @param length - Number of bytes to read
   * @param callback - Completion callback with error and data
   * @returns InEndpoint instance for chaining
   */
  transfer(length: number, callback: (error?: LibUSBException, data?: Buffer) => void): InEndpoint;
  
  /**
   * Async version of transfer
   * @param length - Number of bytes to read
   * @returns Promise resolving to Buffer or undefined
   */
  transferAsync(length: number): Promise<Buffer | undefined>;
}

Usage Examples:

import { findByIds } from 'usb';

const device = findByIds(0x1234, 0x5678);
if (device) {
  device.open();
  const interface0 = device.interface(0);
  interface0.claim();
  
  // Find an IN endpoint
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in');
  if (inEndpoint) {
    console.log(`Using IN endpoint: 0x${inEndpoint.address.toString(16)}`);
    
    // Single transfer - callback style
    inEndpoint.transfer(64, (error, data) => {
      if (error) {
        console.error('Transfer failed:', error.message);
        return;
      }
      
      if (data) {
        console.log(`Received ${data.length} bytes:`, data.toString('hex'));
      }
      
      interface0.release(() => device.close());
    });
  }
}

// Using async/await
async function readDataAsync() {
  const device = findByIds(0x1234, 0x5678);
  if (!device) return;
  
  device.open();
  
  try {
    const interface0 = device.interface(0);
    interface0.claim();
    
    const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
    if (inEndpoint) {
      // Read 128 bytes
      const data = await inEndpoint.transferAsync(128);
      if (data) {
        console.log(`Received ${data.length} bytes`);
        console.log('Data:', data.toString('hex'));
      }
    }
    
    await interface0.releaseAsync();
  } catch (error) {
    console.error('Read error:', error);
  } finally {
    device.close();
  }
}

// Reading specific data lengths
const deviceWithMultipleReads = findByIds(0x5678, 0x1234);
if (deviceWithMultipleReads) {
  deviceWithMultipleReads.open();
  const interface0 = deviceWithMultipleReads.interface(0);
  interface0.claim();
  
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
  if (inEndpoint) {
    // Set custom timeout
    inEndpoint.timeout = 2000; // 2 seconds
    
    // Read different amounts of data
    const readSizes = [8, 16, 32, 64, 128];
    let readIndex = 0;
    
    function readNext() {
      if (readIndex >= readSizes.length) {
        interface0.release(() => deviceWithMultipleReads.close());
        return;
      }
      
      const size = readSizes[readIndex++];
      inEndpoint.transfer(size, (error, data) => {
        if (error) {
          console.error(`Failed to read ${size} bytes:`, error.message);
        } else if (data) {
          console.log(`Read ${size} bytes: ${data.length} received`);
        }
        
        // Read next size
        setTimeout(readNext, 100);
      });
    }
    
    readNext();
  }
}

OUT Endpoint Transfers

Send data to USB devices using OUT endpoints.

/**
 * OUT Endpoint for sending data to device
 */
interface OutEndpoint extends Endpoint {
  direction: 'out';
  
  /**
   * Perform a transfer to write data to the endpoint
   * If length is greater than maxPacketSize, libusb will automatically split 
   * the transfer in multiple packets
   * @param buffer - Data to write
   * @param callback - Completion callback with error and bytes written
   * @returns OutEndpoint instance for chaining
   */
  transfer(buffer: Buffer, callback?: (error?: LibUSBException, actual: number) => void): OutEndpoint;
  
  /**
   * Async version of transfer
   * @param buffer - Data to write
   * @returns Promise resolving to number of bytes written
   */
  transferAsync(buffer: Buffer): Promise<number>;
  
  /**
   * Transfer with Zero Length Packet (ZLP)
   * If buffer length is multiple of max packet size, sends additional ZLP
   * @param buffer - Data to write
   * @param callback - Completion callback
   */
  transferWithZLP(buffer: Buffer, callback: (error?: LibUSBException) => void): void;
}

Usage Examples:

import { findByIds } from 'usb';

const device = findByIds(0x1234, 0x5678);
if (device) {
  device.open();
  const interface0 = device.interface(0);
  interface0.claim();
  
  // Find an OUT endpoint
  const outEndpoint = interface0.endpoints.find(ep => ep.direction === 'out') as OutEndpoint;
  if (outEndpoint) {
    console.log(`Using OUT endpoint: 0x${outEndpoint.address.toString(16)}`);
    
    // Send data - callback style
    const data = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]);
    outEndpoint.transfer(data, (error, bytesWritten) => {
      if (error) {
        console.error('Transfer failed:', error.message);
        return;
      }
      
      console.log(`Sent ${bytesWritten} bytes successfully`);
      interface0.release(() => device.close());
    });
  }
}

// Using async/await
async function sendDataAsync() {
  const device = findByIds(0x1234, 0x5678);
  if (!device) return;
  
  device.open();
  
  try {
    const interface0 = device.interface(0);
    interface0.claim();
    
    const outEndpoint = interface0.endpoints.find(ep => ep.direction === 'out') as OutEndpoint;
    if (outEndpoint) {
      // Send text data
      const textData = Buffer.from('Hello USB Device!', 'utf8');
      const bytesWritten = await outEndpoint.transferAsync(textData);
      console.log(`Sent ${bytesWritten} bytes of text`);
      
      // Send binary data
      const binaryData = Buffer.from([0xFF, 0x00, 0xAA, 0x55, 0x12, 0x34]);
      const bytesSent = await outEndpoint.transferAsync(binaryData);
      console.log(`Sent ${bytesSent} bytes of binary data`);
    }
    
    await interface0.releaseAsync();
  } catch (error) {
    console.error('Send error:', error);
  } finally {
    device.close();
  }
}

// Using Zero Length Packet transfers
const deviceWithZLP = findByIds(0x5678, 0x1234);
if (deviceWithZLP) {
  deviceWithZLP.open();
  const interface0 = deviceWithZLP.interface(0);
  interface0.claim();
  
  const outEndpoint = interface0.endpoints.find(ep => ep.direction === 'out') as OutEndpoint;
  if (outEndpoint) {
    const maxPacketSize = outEndpoint.descriptor.wMaxPacketSize;
    console.log(`Max packet size: ${maxPacketSize}`);
    
    // Create data that's exactly multiple of max packet size
    const dataSize = maxPacketSize * 2; // 2 packets worth
    const data = Buffer.alloc(dataSize, 0xAA);
    
    // This will send the data + a zero length packet to signal end
    outEndpoint.transferWithZLP(data, (error) => {
      if (error) {
        console.error('ZLP transfer failed:', error.message);
      } else {
        console.log(`Sent ${dataSize} bytes with ZLP`);
      }
      
      interface0.release(() => deviceWithZLP.close());
    });
  }
}

// Chunked data sending
async function sendLargeData(outEndpoint: OutEndpoint, data: Buffer, chunkSize: number = 64) {
  const totalSize = data.length;
  let offset = 0;
  let totalSent = 0;
  
  console.log(`Sending ${totalSize} bytes in chunks of ${chunkSize}`);
  
  while (offset < totalSize) {
    const remainingBytes = totalSize - offset;
    const currentChunkSize = Math.min(chunkSize, remainingBytes);
    const chunk = data.slice(offset, offset + currentChunkSize);
    
    try {
      const bytesSent = await outEndpoint.transferAsync(chunk);
      totalSent += bytesSent;
      offset += currentChunkSize;
      
      console.log(`Sent chunk: ${bytesSent} bytes (total: ${totalSent}/${totalSize})`);
      
      // Small delay between chunks if needed
      await new Promise(resolve => setTimeout(resolve, 10));
      
    } catch (error) {
      console.error(`Failed to send chunk at offset ${offset}:`, error);
      break;
    }
  }
  
  return totalSent;
}

Continuous Polling

Set up continuous data polling for streaming data from IN endpoints.

/**
 * Start polling the endpoint for continuous data flow
 * The library will keep nTransfers transfers of size transferSize pending 
 * in the kernel at all times to ensure continuous data flow
 * @param nTransfers - Number of concurrent transfers (default: 3)
 * @param transferSize - Size of each transfer (default: endpoint maxPacketSize)
 * @param callback - Optional transfer completion callback
 * @returns Array of Transfer objects
 */
startPoll(nTransfers?: number, transferSize?: number, callback?: (error?: LibUSBException, buffer: Buffer, actualLength: number, cancelled: boolean) => void): Transfer[];

/**
 * Stop polling
 * Further data may still be received. The 'end' event is emitted when all transfers complete
 * @param callback - Optional completion callback
 */
stopPoll(callback?: () => void): void;

Usage Examples:

import { findByIds } from 'usb';

const device = findByIds(0x1234, 0x5678);
if (device) {
  device.open();
  const interface0 = device.interface(0);
  interface0.claim();
  
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
  if (inEndpoint) {
    console.log(`Starting polling on endpoint 0x${inEndpoint.address.toString(16)}`);
    
    // Set up event handlers
    inEndpoint.on('data', (data: Buffer) => {
      console.log(`Received ${data.length} bytes:`, data.toString('hex'));
    });
    
    inEndpoint.on('error', (error) => {
      console.error('Polling error:', error.message);
      inEndpoint.stopPoll();
    });
    
    inEndpoint.on('end', () => {
      console.log('Polling ended');
      interface0.release(() => device.close());
    });
    
    // Start polling with 3 concurrent transfers of 64 bytes each
    const transfers = inEndpoint.startPoll(3, 64);
    console.log(`Started polling with ${transfers.length} concurrent transfers`);
    
    // Stop polling after 10 seconds
    setTimeout(() => {
      console.log('Stopping polling...');
      inEndpoint.stopPoll();
    }, 10000);
  }
}

// Advanced polling with custom parameters
const deviceAdvanced = findByIds(0x5678, 0x1234);
if (deviceAdvanced) {
  deviceAdvanced.open();
  const interface0 = deviceAdvanced.interface(0);
  interface0.claim();
  
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
  if (inEndpoint) {
    let totalBytesReceived = 0;
    let packetsReceived = 0;
    
    // Set up data handler
    inEndpoint.on('data', (data: Buffer) => {
      totalBytesReceived += data.length;
      packetsReceived++;
      
      if (packetsReceived % 100 === 0) {
        console.log(`Received ${packetsReceived} packets, ${totalBytesReceived} total bytes`);
      }
    });
    
    // Set up error handler
    inEndpoint.on('error', (error) => {
      console.error('Polling error:', error);
      inEndpoint.stopPoll(() => {
        console.log(`Final stats: ${packetsReceived} packets, ${totalBytesReceived} bytes`);
      });
    });
    
    // Start high-throughput polling
    const maxPacketSize = inEndpoint.descriptor.wMaxPacketSize;
    const nTransfers = 8; // More concurrent transfers for higher throughput
    const transferSize = maxPacketSize * 4; // Larger transfer size
    
    console.log(`Starting high-throughput polling:`);
    console.log(`  Transfers: ${nTransfers}`);
    console.log(`  Transfer size: ${transferSize} bytes`);
    console.log(`  Max packet size: ${maxPacketSize}`);
    
    inEndpoint.startPoll(nTransfers, transferSize, (error, buffer, actualLength, cancelled) => {
      if (error && !cancelled) {
        console.error('Transfer callback error:', error.message);
      }
    });
    
    // Monitor polling status
    const statusInterval = setInterval(() => {
      console.log(`Polling active: ${inEndpoint.pollActive}`);
      if (!inEndpoint.pollActive) {
        clearInterval(statusInterval);
      }
    }, 5000);
    
    // Stop after 30 seconds
    setTimeout(() => {
      inEndpoint.stopPoll(() => {
        console.log('Polling stopped gracefully');
        clearInterval(statusInterval);
        interface0.release(() => deviceAdvanced.close());
      });
    }, 30000);
  }
}

Transfer Management

Create and manage individual transfers for fine-grained control.

/**
 * Create a new Transfer object for this endpoint
 * @param timeout - Timeout for the transfer (0 means unlimited)
 * @param callback - Transfer completion callback
 * @returns Transfer object
 */
makeTransfer(timeout: number, callback: (error?: LibUSBException, buffer: Buffer, actualLength: number) => void): Transfer;

/**
 * Transfer class for USB transfers
 */
class Transfer {
  /**
   * Create a new Transfer object
   * @param device - USB Device object
   * @param endpointAddr - Endpoint address  
   * @param type - Transfer type (BULK, INTERRUPT, etc.)
   * @param timeout - Transfer timeout in milliseconds
   * @param callback - Transfer completion callback
   */
  constructor(
    device: Device, 
    endpointAddr: number, 
    type: number, 
    timeout: number, 
    callback: (error?: LibUSBException, buffer: Buffer, actualLength: number) => void
  );
  
  /**
   * Submit the transfer
   * @param buffer - Buffer for data (IN: where data will be written, OUT: data to send)
   * @param callback - Optional completion callback
   * @returns Transfer instance
   */
  submit(buffer: Buffer, callback?: (error?: LibUSBException, buffer: Buffer, actualLength: number) => void): Transfer;
  
  /**
   * Cancel the transfer
   * @returns true if transfer was canceled, false if it wasn't in pending state
   */
  cancel(): boolean;
}

Usage Examples:

import { findByIds } from 'usb';

const device = findByIds(0x1234, 0x5678);
if (device) {
  device.open();
  const interface0 = device.interface(0);
  interface0.claim();
  
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
  if (inEndpoint) {
    // Create a custom transfer with 5 second timeout
    const transfer = inEndpoint.makeTransfer(5000, (error, buffer, actualLength) => {
      if (error) {
        console.error('Transfer error:', error.message);
      } else {
        console.log(`Transfer completed: ${actualLength} bytes received`);
        console.log('Data:', buffer.slice(0, actualLength).toString('hex'));
      }
      
      interface0.release(() => device.close());
    });
    
    // Submit the transfer with a 128-byte buffer
    const buffer = Buffer.alloc(128);
    transfer.submit(buffer);
    
    console.log('Transfer submitted, waiting for completion...');
    
    // Cancel the transfer after 2 seconds if needed
    setTimeout(() => {
      const cancelled = transfer.cancel();
      if (cancelled) {
        console.log('Transfer was cancelled');
      } else {
        console.log('Transfer was not in pending state (already completed or not submitted)');
      }
    }, 2000);
  }
}

// Multiple concurrent transfers
const deviceConcurrent = findByIds(0x5678, 0x1234);
if (deviceConcurrent) {
  deviceConcurrent.open();
  const interface0 = deviceConcurrent.interface(0);
  interface0.claim();
  
  const inEndpoint = interface0.endpoints.find(ep => ep.direction === 'in') as InEndpoint;
  if (inEndpoint) {
    const transfers: Transfer[] = [];
    let completedTransfers = 0;
    const totalTransfers = 5;
    
    // Create multiple transfers
    for (let i = 0; i < totalTransfers; i++) {
      const transfer = inEndpoint.makeTransfer(3000, (error, buffer, actualLength) => {
        completedTransfers++;
        
        if (error) {
          console.error(`Transfer ${i} error:`, error.message);
        } else {
          console.log(`Transfer ${i} completed: ${actualLength} bytes`);
        }
        
        // Clean up when all transfers complete
        if (completedTransfers === totalTransfers) {
          console.log('All transfers completed');
          interface0.release(() => deviceConcurrent.close());
        }
      });
      
      transfers.push(transfer);
      
      // Submit each transfer
      const buffer = Buffer.alloc(64);
      transfer.submit(buffer);
    }
    
    console.log(`Submitted ${totalTransfers} concurrent transfers`);
    
    // Cancel all transfers after 10 seconds if needed
    setTimeout(() => {
      console.log('Cancelling all pending transfers...');
      transfers.forEach((transfer, index) => {
        const cancelled = transfer.cancel();
        if (cancelled) {
          console.log(`Transfer ${index} cancelled`);
        }
      });
    }, 10000);
  }
}

// Transfer with retry logic
async function transferWithRetry(endpoint: InEndpoint, length: number, maxRetries: number = 3): Promise<Buffer | null> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Transfer attempt ${attempt}/${maxRetries}`);
      
      const data = await endpoint.transferAsync(length);
      if (data) {
        console.log(`Transfer succeeded on attempt ${attempt}`);
        return data;
      }
    } catch (error) {
      console.error(`Transfer attempt ${attempt} failed:`, error);
      
      if (attempt === maxRetries) {
        console.error('All transfer attempts failed');
        return null;
      }
      
      // Wait before retry
      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
    }
  }
  
  return null;
}

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