UNPKG

usb

Version:
429 lines 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebUSBDevice = void 0; const usb = require("../usb"); const util_1 = require("util"); const os_1 = require("os"); const LIBUSB_TRANSFER_TYPE_MASK = 0x03; const ENDPOINT_NUMBER_MASK = 0x7f; const CLEAR_FEATURE = 0x01; const ENDPOINT_HALT = 0x00; /** * Wrapper to make a node-usb device look like a webusb device */ class WebUSBDevice { static async createInstance(device, autoDetachKernelDriver = true) { const instance = new WebUSBDevice(device, autoDetachKernelDriver); await instance.initialize(); return instance; } constructor(device, autoDetachKernelDriver) { this.device = device; this.autoDetachKernelDriver = autoDetachKernelDriver; this.configurations = []; const usbVersion = this.decodeVersion(device.deviceDescriptor.bcdUSB); this.usbVersionMajor = usbVersion.major; this.usbVersionMinor = usbVersion.minor; this.usbVersionSubminor = usbVersion.sub; this.deviceClass = device.deviceDescriptor.bDeviceClass; this.deviceSubclass = device.deviceDescriptor.bDeviceSubClass; this.deviceProtocol = device.deviceDescriptor.bDeviceProtocol; this.vendorId = device.deviceDescriptor.idVendor; this.productId = device.deviceDescriptor.idProduct; const deviceVersion = this.decodeVersion(device.deviceDescriptor.bcdDevice); this.deviceVersionMajor = deviceVersion.major; this.deviceVersionMinor = deviceVersion.minor; this.deviceVersionSubminor = deviceVersion.sub; this.controlTransferAsync = (0, util_1.promisify)(this.device.controlTransfer).bind(this.device); this.setConfigurationAsync = (0, util_1.promisify)(this.device.setConfiguration).bind(this.device); this.resetAsync = (0, util_1.promisify)(this.device.reset).bind(this.device); this.getStringDescriptorAsync = (0, util_1.promisify)(this.device.getStringDescriptor).bind(this.device); } get configuration() { if (!this.device.configDescriptor) { return undefined; } const currentConfiguration = this.device.configDescriptor.bConfigurationValue; return this.configurations.find(configuration => configuration.configurationValue === currentConfiguration); } get opened() { return (!!this.device.interfaces); } async open() { try { if (this.opened) { return; } this.device.open(); this.device.setAutoDetachKernelDriver(this.autoDetachKernelDriver); } catch (error) { throw new Error(`open error: ${error}`); } } async close() { try { if (!this.opened) { return; } try { if (this.configuration) { for (const iface of this.configuration.interfaces) { await this._releaseInterface(iface.interfaceNumber); // Re-create the USBInterface to set the claimed attribute this.configuration.interfaces[this.configuration.interfaces.indexOf(iface)] = { interfaceNumber: iface.interfaceNumber, alternate: iface.alternate, alternates: iface.alternates, claimed: false }; } } } catch (_error) { // Ignore } this.device.close(); } catch (error) { throw new Error(`close error: ${error}`); } } async selectConfiguration(configurationValue) { if (!this.opened || !this.device.configDescriptor) { throw new Error('selectConfiguration error: invalid state'); } if (this.device.configDescriptor.bConfigurationValue === configurationValue) { return; } const config = this.configurations.find(configuration => configuration.configurationValue === configurationValue); if (!config) { throw new Error('selectConfiguration error: configuration not found'); } try { await this.setConfigurationAsync(configurationValue); } catch (error) { throw new Error(`selectConfiguration error: ${error}`); } } async claimInterface(interfaceNumber) { if (!this.opened) { throw new Error('claimInterface error: invalid state'); } if (!this.configuration) { throw new Error('claimInterface error: interface not found'); } const iface = this.configuration.interfaces.find(usbInterface => usbInterface.interfaceNumber === interfaceNumber); if (!iface) { throw new Error('claimInterface error: interface not found'); } if (iface.claimed) { return; } try { this.device.interface(interfaceNumber).claim(); // Re-create the USBInterface to set the claimed attribute this.configuration.interfaces[this.configuration.interfaces.indexOf(iface)] = { interfaceNumber, alternate: iface.alternate, alternates: iface.alternates, claimed: true }; } catch (error) { throw new Error(`claimInterface error: ${error}`); } } async releaseInterface(interfaceNumber) { await this._releaseInterface(interfaceNumber); if (this.configuration) { const iface = this.configuration.interfaces.find(usbInterface => usbInterface.interfaceNumber === interfaceNumber); if (iface) { // Re-create the USBInterface to set the claimed attribute this.configuration.interfaces[this.configuration.interfaces.indexOf(iface)] = { interfaceNumber, alternate: iface.alternate, alternates: iface.alternates, claimed: false }; } } } async selectAlternateInterface(interfaceNumber, alternateSetting) { if (!this.opened) { throw new Error('selectAlternateInterface error: invalid state'); } if (!this.configuration) { throw new Error('selectAlternateInterface error: interface not found'); } const iface = this.configuration.interfaces.find(usbInterface => usbInterface.interfaceNumber === interfaceNumber); if (!iface) { throw new Error('selectAlternateInterface error: interface not found'); } if (!iface.claimed) { throw new Error('selectAlternateInterface error: invalid state'); } try { const iface = this.device.interface(interfaceNumber); await iface.setAltSettingAsync(alternateSetting); } catch (error) { throw new Error(`selectAlternateInterface error: ${error}`); } } async controlTransferIn(setup, length) { try { this.checkDeviceOpen(); const type = this.controlTransferParamsToType(setup, usb.LIBUSB_ENDPOINT_IN); const result = await this.controlTransferAsync(type, setup.request, setup.value, setup.index, length); return { data: result ? new DataView(new Uint8Array(result).buffer) : undefined, status: 'ok' }; } catch (error) { if (error.errno === usb.LIBUSB_TRANSFER_STALL) { return { status: 'stall' }; } if (error.errno === usb.LIBUSB_TRANSFER_OVERFLOW) { return { status: 'babble' }; } throw new Error(`controlTransferIn error: ${error}`); } } async controlTransferOut(setup, data) { try { this.checkDeviceOpen(); const type = this.controlTransferParamsToType(setup, usb.LIBUSB_ENDPOINT_OUT); const buffer = data ? Buffer.from(data) : Buffer.alloc(0); const bytesWritten = await this.controlTransferAsync(type, setup.request, setup.value, setup.index, buffer); return { bytesWritten, status: 'ok' }; } catch (error) { if (error.errno === usb.LIBUSB_TRANSFER_STALL) { return { bytesWritten: 0, status: 'stall' }; } throw new Error(`controlTransferOut error: ${error}`); } } async clearHalt(direction, endpointNumber) { try { const wIndex = endpointNumber | (direction === 'in' ? usb.LIBUSB_ENDPOINT_IN : usb.LIBUSB_ENDPOINT_OUT); await this.controlTransferAsync(usb.LIBUSB_RECIPIENT_ENDPOINT, CLEAR_FEATURE, ENDPOINT_HALT, wIndex, Buffer.from(new Uint8Array())); } catch (error) { throw new Error(`clearHalt error: ${error}`); } } async transferIn(endpointNumber, length) { try { this.checkDeviceOpen(); const endpoint = this.getEndpoint(endpointNumber | usb.LIBUSB_ENDPOINT_IN); const result = await endpoint.transferAsync(length); return { data: result ? new DataView(new Uint8Array(result).buffer) : undefined, status: 'ok' }; } catch (error) { if (error.errno === usb.LIBUSB_TRANSFER_STALL) { return { status: 'stall' }; } if (error.errno === usb.LIBUSB_TRANSFER_OVERFLOW) { return { status: 'babble' }; } throw new Error(`transferIn error: ${error}`); } } async transferOut(endpointNumber, data) { try { this.checkDeviceOpen(); const endpoint = this.getEndpoint(endpointNumber | usb.LIBUSB_ENDPOINT_OUT); const buffer = Buffer.from(data); const bytesWritten = await endpoint.transferAsync(buffer); return { bytesWritten, status: 'ok' }; } catch (error) { if (error.errno === usb.LIBUSB_TRANSFER_STALL) { return { bytesWritten: 0, status: 'stall' }; } throw new Error(`transferOut error: ${error}`); } } async reset() { try { await this.resetAsync(); } catch (error) { throw new Error(`reset error: ${error}`); } } async isochronousTransferIn(_endpointNumber, _packetLengths) { throw new Error('isochronousTransferIn error: method not implemented'); } async isochronousTransferOut(_endpointNumber, _data, _packetLengths) { throw new Error('isochronousTransferOut error: method not implemented'); } async forget() { throw new Error('forget error: method not implemented'); } async initialize() { try { if (!this.opened) { this.device.open(); // Explicitly set configuration for vendor-specific devices on macos // https://github.com/node-usb/node-usb/issues/61 if (this.deviceClass === 0xff && (0, os_1.platform)() === 'darwin') { await this.setConfigurationAsync(1); } } this.manufacturerName = await this.getStringDescriptor(this.device.deviceDescriptor.iManufacturer); this.productName = await this.getStringDescriptor(this.device.deviceDescriptor.iProduct); this.serialNumber = await this.getStringDescriptor(this.device.deviceDescriptor.iSerialNumber); this.configurations = await this.getConfigurations(); } catch (error) { throw new Error(`initialize error: ${error}`); } finally { if (this.opened) { this.device.close(); } } } decodeVersion(version) { const hex = `0000${version.toString(16)}`.slice(-4); return { major: parseInt(hex.substr(0, 2), undefined), minor: parseInt(hex.substr(2, 1), undefined), sub: parseInt(hex.substr(3, 1), undefined), }; } async getStringDescriptor(index) { try { const buffer = await this.getStringDescriptorAsync(index); return buffer ? buffer.toString() : ''; } catch (error) { return ''; } } async getConfigurations() { const configs = []; for (const config of this.device.allConfigDescriptors) { const interfaces = []; for (const iface of config.interfaces) { const alternates = []; for (const alternate of iface) { const endpoints = []; for (const endpoint of alternate.endpoints) { endpoints.push({ endpointNumber: endpoint.bEndpointAddress & ENDPOINT_NUMBER_MASK, direction: endpoint.bEndpointAddress & usb.LIBUSB_ENDPOINT_IN ? 'in' : 'out', type: (endpoint.bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) === usb.LIBUSB_TRANSFER_TYPE_BULK ? 'bulk' : (endpoint.bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) === usb.LIBUSB_TRANSFER_TYPE_INTERRUPT ? 'interrupt' : 'isochronous', packetSize: endpoint.wMaxPacketSize }); } alternates.push({ alternateSetting: alternate.bAlternateSetting, interfaceClass: alternate.bInterfaceClass, interfaceSubclass: alternate.bInterfaceSubClass, interfaceProtocol: alternate.bInterfaceProtocol, interfaceName: await this.getStringDescriptor(alternate.iInterface), endpoints }); } const interfaceNumber = iface[0].bInterfaceNumber; const alternate = alternates.find(alt => alt.alternateSetting === this.device.interface(interfaceNumber).altSetting); if (alternate) { interfaces.push({ interfaceNumber, alternate, alternates, claimed: false }); } } configs.push({ configurationValue: config.bConfigurationValue, configurationName: await this.getStringDescriptor(config.iConfiguration), interfaces }); } return configs; } getEndpoint(address) { if (!this.device.interfaces) { return undefined; } for (const iface of this.device.interfaces) { const endpoint = iface.endpoint(address); if (endpoint) { return endpoint; } } return undefined; } controlTransferParamsToType(setup, direction) { const recipient = setup.recipient === 'device' ? usb.LIBUSB_RECIPIENT_DEVICE : setup.recipient === 'interface' ? usb.LIBUSB_RECIPIENT_INTERFACE : setup.recipient === 'endpoint' ? usb.LIBUSB_RECIPIENT_ENDPOINT : usb.LIBUSB_RECIPIENT_OTHER; const requestType = setup.requestType === 'standard' ? usb.LIBUSB_REQUEST_TYPE_STANDARD : setup.requestType === 'class' ? usb.LIBUSB_REQUEST_TYPE_CLASS : usb.LIBUSB_REQUEST_TYPE_VENDOR; return recipient | requestType | direction; } async _releaseInterface(interfaceNumber) { if (!this.opened) { throw new Error('releaseInterface error: invalid state'); } if (!this.configuration) { throw new Error('releaseInterface error: interface not found'); } const iface = this.configuration.interfaces.find(usbInterface => usbInterface.interfaceNumber === interfaceNumber); if (!iface) { throw new Error('releaseInterface error: interface not found'); } if (!iface.claimed) { return; } try { const iface = this.device.interface(interfaceNumber); await iface.releaseAsync(); } catch (error) { throw new Error(`releaseInterface error: ${error}`); } } checkDeviceOpen() { if (!this.opened) { throw new Error('The device must be opened first'); } } } exports.WebUSBDevice = WebUSBDevice; //# sourceMappingURL=webusb-device.js.map