usb
Version:
Library to access USB devices
429 lines • 17.3 kB
JavaScript
"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