UNPKG

@elgato-stream-deck/tcp

Version:

An npm module for interfacing with select Elgato Stream Deck devices in node over tcp

160 lines 6.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TcpCoraHidDevice = void 0; const EventEmitter = require("events"); const core_1 = require("@elgato-stream-deck/core"); const socketWrapper_js_1 = require("../socketWrapper.js"); const device2Info_js_1 = require("../device2Info.js"); const util_js_1 = require("./util.js"); /** * A HIDDevice implementation for cora based TCP connections * This isn't really HID, but it fits the existing structure well enough * Note: this gets destroyed when the socket is closed, so we can rely on this for resetting the state */ class TcpCoraHidDevice extends EventEmitter { #socket; #isPrimary = true; #onChildInfoChange = null; get isPrimary() { return this.#isPrimary; } set onChildInfoChange(cb) { this.#onChildInfoChange = cb; } constructor(socket) { super(); this.#socket = socket; this.#socket.on('dataCora', (data) => { let singletonCommand; if (data.payload[0] === 0x01 && data.payload[1] === 0x0b) { // Query about Device 2 singletonCommand = this.#pendingSingletonCommands.get(0x1c); if (!singletonCommand && this.#onChildInfoChange) { // If there is no command, this is a plug event this.#onChildInfoChange((0, device2Info_js_1.parseDevice2Info)(data.payload)); } } else if (data.payload[0] === 0x01) { this.emit('input', data.payload.subarray(1)); } else if (data.payload[0] === 0x03) { // Command for the Studio port singletonCommand = this.#pendingSingletonCommands.get(data.payload[1]); } else { // Command for the Device 2 port singletonCommand = this.#pendingSingletonCommands.get(data.payload[0]); } if (singletonCommand) { const singletonCommand0 = singletonCommand; setImmediate(() => singletonCommand0.resolve(data.payload)); } }); this.#socket.on('error', (message, err) => this.emit('error', `Socket error: ${message} (${err?.message ?? err})`)); this.#socket.on('disconnected', () => { for (const command of this.#pendingSingletonCommands.values()) { try { command.reject(new Error('Disconnected')); } catch (_e) { // Ignore } } this.#pendingSingletonCommands.clear(); }); } async close() { throw new Error('Socket is owned by the connection manager, and cannot be closed directly'); // await this.#socket.close() } async sendFeatureReport(data) { this.#socket.sendCoraWrites([ { flags: socketWrapper_js_1.CoraMessageFlags.VERBATIM, hidOp: socketWrapper_js_1.CoraHidOp.SEND_REPORT, messageId: 0, payload: Buffer.from(data), }, ]); } async getFeatureReport(reportId, _reportLength) { return this.#executeSingletonCommand(reportId, this.#isPrimary); } #pendingSingletonCommands = new Map(); async #executeSingletonCommand(commandType, toHost) { // if (!this.connected) throw new Error('Not connected') const messageId = Math.floor(Math.random() * 0xffffff); // Random message ID for Cora const msg = { flags: toHost ? socketWrapper_js_1.CoraMessageFlags.NONE : socketWrapper_js_1.CoraMessageFlags.VERBATIM, hidOp: socketWrapper_js_1.CoraHidOp.GET_REPORT, messageId: messageId, payload: toHost ? Buffer.from([0x03, commandType]) : Buffer.from([commandType]), }; const command = new util_js_1.QueuedCommand(commandType); this.#pendingSingletonCommands.set(commandType, command); command.promise .finally(() => { this.#pendingSingletonCommands.delete(commandType); }) .catch(() => null); this.#socket.sendCoraWrites([msg]); // TODO - improve this timeout setTimeout(() => { command.reject(new Error('Timeout')); }, 5000); return command.promise; } async sendReports(buffers) { this.#socket.sendCoraWrites(buffers.map((buffer) => ({ flags: socketWrapper_js_1.CoraMessageFlags.VERBATIM, hidOp: socketWrapper_js_1.CoraHidOp.WRITE, messageId: 0, payload: buffer, }))); } #loadedHidInfo; async getDeviceInfo() { // Cache once loaded. This is a bit of a race condition, but with minimal impact as we already run it before handling the class off anywhere if (this.#loadedHidInfo) return this.#loadedHidInfo; const deviceInfo = await Promise.race([ // primary port this.#executeSingletonCommand(0x80, true).then((data) => ({ data, isPrimary: true })), // secondary port this.#executeSingletonCommand(0x08, false).then((data) => ({ data, isPrimary: false })), ]); // Future: this internal mutation is a bit of a hack, but it avoids needing to duplicate the singleton logic this.#isPrimary = deviceInfo.isPrimary; const devicePath = `tcp://${this.#socket.address}:${this.#socket.port}`; if (this.#isPrimary) { const dataView = (0, core_1.uint8ArrayToDataView)(deviceInfo.data); const vendorId = dataView.getUint16(12, true); const productId = dataView.getUint16(14, true); this.#loadedHidInfo = { vendorId: vendorId, productId: productId, path: devicePath, }; } else { const rawDevice2Info = await this.#executeSingletonCommand(0x1c, true); const device2Info = (0, device2Info_js_1.parseDevice2Info)(rawDevice2Info); if (!device2Info) throw new Error('Failed to get Device info'); this.#loadedHidInfo = { vendorId: device2Info.vendorId, productId: device2Info.productId, path: devicePath, }; } return this.#loadedHidInfo; } async getChildDeviceInfo() { if (!this.#isPrimary) return null; const device2Info = await this.#executeSingletonCommand(0x1c, true); return (0, device2Info_js_1.parseDevice2Info)(device2Info); } } exports.TcpCoraHidDevice = TcpCoraHidDevice; //# sourceMappingURL=cora.js.map