@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
JavaScript
"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