UNPKG

@huddly/device-api-usb

Version:

Huddly SDK device api which uses node-usb wrapper responsible for handling the transport layer of the communication and discovering the physical device/camera

374 lines 15.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const usb_1 = require("usb"); const endpoint_1 = require("usb/dist/usb/endpoint"); const Logger_1 = __importDefault(require("@huddly/sdk-interfaces/lib/statics/Logger")); const messagepacket_1 = __importDefault(require("./messagepacket")); class NodeUsbTransport extends events_1.EventEmitter { constructor(device) { super(); this.MAX_PACKET_SIZE = 16 * 1024; this.VSC_INTERFACE_CLASS = 255; // Vendor Specifc Class this.READ_STATES = Object.freeze({ NEW_READ: 'new_read', PENDING_CHUNK: 'pending_chunk', }); this.className = 'Device-API-USB Transport'; this.isPollingActive = false; this.hlinkProtocolVersion = 'HLink v0'; /***** Read Event Loop Helper Variables ******/ this.readLoopChunks = []; /** * A boolean representation of the device being opened and its corresponding * vsc interface claimed for channeling the communication. * * @private * @type {boolean} * @memberof NodeUsbTransport */ this.deviceClaimed = false; this._device = device; super.setMaxListeners(50); } /** * Getter method for device class attribute. * * @type {*} * @memberof NodeUsbTransport */ get device() { return this._device; } /** * Set method for device class attribute. * * @memberof NodeUsbTransport */ set device(device) { this._device = device; } init() { if (this.deviceClaimed) { return; } let opened = false; return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { try { this.device.open(); opened = true; const vscInterface = this.device.interfaces.find((ifc) => ifc.descriptor.bInterfaceClass === this.VSC_INTERFACE_CLASS); if (!vscInterface) return reject('No VSC Interface present on the usb device!'); this.vscInterface = vscInterface; this.vscInterface.claim(); this.inEndpoint = this.vscInterface.endpoints.find((endpoint) => endpoint instanceof endpoint_1.InEndpoint); this.outEndpoint = this.vscInterface.endpoints.find((endpoint) => endpoint instanceof endpoint_1.OutEndpoint); this.deviceClaimed = true; resolve(); } catch (err) { if (opened) { yield this.close(); } if (err.errno === usb_1.usb.LIBUSB_ERROR_ACCESS) { Logger_1.default.warn('Unable to claim usb interface. Please make sure the device is not used by another process!', this.className); return reject(`Unable to claim usb interface. Please make sure the device is not used by another process!`); } Logger_1.default.warn('Unable to initialize NodeUsbTransport!', this.className); return reject(err); } })); } initEventLoop() { var _a; if (!this.isPollingActive) { Logger_1.default.debug('Starting event loop!', this.className); if (((_a = this.inEndpoint.descriptor) === null || _a === void 0 ? void 0 : _a.wMaxPacketSize) == undefined) { throw new Error('InEndpoint does not contain information about max packet size!'); } this.inEndpoint.startPoll(1, this.inEndpoint.descriptor.wMaxPacketSize); this.startListen(); this.isPollingActive = true; } } performHlinkHandshake() { return __awaiter(this, void 0, void 0, function* () { yield this.sendChunk(Buffer.alloc(0)); yield this.sendChunk(Buffer.alloc(1, 0x00)); const res = yield this.readChunk(1024); const decodedMsg = res.toString('utf8'); if (decodedMsg !== this.hlinkProtocolVersion) { const message = `Hlink handshake has failed! Wrong version. Expected ${this.hlinkProtocolVersion}, got ${decodedMsg}.`; return Promise.reject(message); } return Promise.resolve(); }); } readLoopReset() { this.readLoopChunks.splice(0, this.readLoopChunks.length); this.currentBufferReadSize = 0; this.expectedReadBufferSize = -1; this.currentStateOfReadLoop = this.READ_STATES.NEW_READ; } parseAndEmitFullyRetrievedMessage() { const finalBuffer = Buffer.concat(this.readLoopChunks); const result = messagepacket_1.default.parseMessage(finalBuffer); this.emit(result.message, result); this.readLoopReset(); } continueReadLogic() { if (this.currentBufferReadSize < this.expectedReadBufferSize) { this.currentStateOfReadLoop = this.READ_STATES.PENDING_CHUNK; } else { this.parseAndEmitFullyRetrievedMessage(); } } onDataRetrievedHandler(buffer) { var _a; if (this.currentStateOfReadLoop === this.READ_STATES.NEW_READ) { if (buffer.length < messagepacket_1.default.HEADER_SIZES.HDR_SIZE) { (_a = this.inEndpoint) === null || _a === void 0 ? void 0 : _a.removeListener('data', this.onDataRetrievedHandler); if (buffer.length === 8 && buffer.toString('utf8') === this.hlinkProtocolVersion) { throw new Error('Received a hlink reset sequence. Read loop cannot continue!'); } throw new Error(`Received an incomplete message on start of read! Message size: ${buffer.length}. Unable to proceed, exiting ungracefully!`); } this.expectedReadBufferSize = messagepacket_1.default.parseMessage(buffer).totalSize(); this.readLoopChunks = [buffer]; this.currentBufferReadSize = buffer.length; this.continueReadLogic(); } else { this.readLoopChunks.push(Buffer.from(buffer)); this.currentBufferReadSize += buffer.length; this.continueReadLogic(); } } startListen() { this.readLoopReset(); const closeHandler = () => { var _a; Logger_1.default.debug('Removing transport data event subscription.', this.className); (_a = this.inEndpoint) === null || _a === void 0 ? void 0 : _a.removeListener('data', this.onDataRetrievedHandler); }; const errorHandler = (error) => { if (error.errno != usb_1.usb.LIBUSB_TRANSFER_NO_DEVICE) { Logger_1.default.error(`Received error message on read loop!`, error, this.className); } this.close(); }; // Setup event handlers this.inEndpoint.on('data', this.onDataRetrievedHandler.bind(this)); this.inEndpoint.once('error', errorHandler); this.once('ERROR', errorHandler); this.once('CLOSED', closeHandler); } receiveMessage(msg, timeout = 500) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { try { this.removeAllListeners(msg); reject(`Request has timed out! ${msg} ${timeout}`); } finally { clearTimeout(timer); } }, timeout); const messageHandler = (res) => { clearTimeout(timer); this.removeListener('ERROR', errorHandler); resolve(res); }; const errorHandler = (error) => { clearTimeout(timer); this.removeListener(msg, messageHandler); reject(error); }; this.once(msg, messageHandler); this.once('ERROR', errorHandler); }); } write(cmd, payload = Buffer.alloc(0)) { const encodedMsgBuffer = messagepacket_1.default.createMessage(cmd, payload); return new Promise((resolve, reject) => { this.sendChunk(encodedMsgBuffer) .then((_) => resolve()) .catch((e) => { if (e.message.includes('LIBUSB_ERROR_IO') && encodedMsgBuffer.length > this.MAX_PACKET_SIZE) { this.splitAndSendPayloadInChunks(encodedMsgBuffer) .then((_) => resolve()) .catch((e) => reject(e)); } else { reject(e); } }); }); } splitAndSendPayloadInChunks(payload) { return __awaiter(this, void 0, void 0, function* () { for (let i = 0; i < payload.length; i += this.MAX_PACKET_SIZE) { const chunk = payload.slice(i, i + this.MAX_PACKET_SIZE); yield this.sendChunk(chunk); } }); } subscribe(command) { return this.write('hlink-mb-subscribe', command); } unsubscribe(command) { return this.write('hlink-mb-unsubscribe', command); } readChunk(packetSize = this.MAX_PACKET_SIZE) { return __awaiter(this, void 0, void 0, function* () { if (!this.inEndpoint) return Promise.reject('Device inEndpoint not initialized!'); return new Promise((resolve, reject) => { this.inEndpoint.transfer(packetSize, (err, data) => { if (err) return reject(new Error(`Unable to read data from device (LibUSBException: ${err.errno})! ${err.message}`)); resolve(data); }); }); }); } sendChunk(chunk) { return __awaiter(this, void 0, void 0, function* () { if (!this.outEndpoint) return Promise.reject('Device outEndpoint not initialized!'); return new Promise((resolve, reject) => { this.outEndpoint.transfer(chunk, (err) => { if (err) return reject(new Error(`Unable to write data to device (LibUSBException: ${err.errno})! ${err.message}`)); resolve(); }); }); }); } /********* Teardown Methods *********/ stopUsbEndpointPoll() { return __awaiter(this, void 0, void 0, function* () { if (this.inEndpoint && !this.inEndpoint.pollActive) { this.isPollingActive = false; return; } return new Promise((resolve, reject) => { this.inEndpoint.stopPoll(() => { this.isPollingActive = false; resolve(); }); this.inEndpoint.once('error', (error) => { if (error.errno != usb_1.usb.LIBUSB_TRANSFER_NO_DEVICE) { Logger_1.default.error('Unable to stop poll!', error, this.className); reject(error); } }); }); }); } stopEventLoop() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { this.removeAllListeners(); if (this.isPollingActive) { this.stopUsbEndpointPoll() .then((_) => resolve()) .catch((e) => reject(e)); } else { resolve(); } }); }); } releaseEndpoints() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { if (this.deviceClaimed) { this.stopUsbEndpointPoll() .then(() => { this.vscInterface.release(true, (err) => { if (err) { if (err.errno !== usb_1.usb.LIBUSB_ERROR_NO_DEVICE) // Ignore LIBUSB_ERROR_NO_DEVICE since we are releasing/closing device already return reject(`Unable to release vsc interface! UsbError: ${err.errno} \n${err.stack || err.message}`); } resolve(); }); }) .catch((e) => reject(e)); } else { resolve(); } }); }); } close() { return __awaiter(this, void 0, void 0, function* () { if (!this.deviceClaimed) { return; } return new Promise((resolve, reject) => { this.releaseEndpoints() .then((_) => { try { this.device.close(); } catch (_a) { } }) .then((_) => { this.deviceClaimed = false; this.emit('CLOSED'); resolve(); }) .catch(reject); }); }); } /********* EventEmitter Overrides *********/ once(eventName, listener) { super.on(eventName, listener); return this; } on(eventName, listener) { super.on(eventName, listener); return this; } removeListener(eventName, listener) { super.removeListener(eventName, listener); return this; } removeAllListeners(eventName) { super.removeAllListeners(eventName); return this; } /********* DEPRECATED/LEGACY METHODS *********/ receive() { throw new Error('Method "receive" is no longer supported! Please use "receiveMessage" instead.'); } read(receiveMsg = 'unknown', timeout = 500) { throw new Error('Method "read" is no longer supported! Please use "receiveMessage" instead.'); } setEventLoopReadSpeed(timeout = 0) { } clear() { return Promise.resolve(); } } exports.default = NodeUsbTransport; //# sourceMappingURL=transport.js.map