UNPKG

@elgato-stream-deck/tcp

Version:

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

157 lines 6.38 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TcpLegacyHidDevice = void 0; const EventEmitter = require("events"); const core_1 = require("@elgato-stream-deck/core"); const device2Info_js_1 = require("../device2Info.js"); const util_js_1 = require("./util.js"); /** * A HIDDevice implementation for 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 TcpLegacyHidDevice 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('dataLegacy', (data) => { let singletonCommand; if (data[0] === 0x01 && data[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)); } } else if (data[0] === 0x01) { this.emit('input', data.subarray(1)); } else if (data[0] === 0x03) { // Command for the Studio port singletonCommand = this.#pendingSingletonCommands.get(data[1]); } else { // Command for the Device 2 port singletonCommand = this.#pendingSingletonCommands.get(data[0]); } if (singletonCommand) { const singletonCommand0 = singletonCommand; setImmediate(() => singletonCommand0.resolve(data)); } }); 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) { // Ensure the buffer is 1024 bytes long let dataFull = data; if (data.length != 1024) { dataFull = new Uint8Array(1024); dataFull.set(data.slice(0, Math.min(data.length, dataFull.length))); } this.#socket.sendLegacyWrites([dataFull]); } async getFeatureReport(reportId, _reportLength) { return this.#executeSingletonCommand(reportId, this.#isPrimary); } #pendingSingletonCommands = new Map(); async #executeSingletonCommand(commandType, isPrimary) { // if (!this.connected) throw new Error('Not connected') const existingCommand = this.#pendingSingletonCommands.get(commandType); if (existingCommand) return existingCommand.promise; const command = new util_js_1.QueuedCommand(commandType); this.#pendingSingletonCommands.set(commandType, command); command.promise .finally(() => { this.#pendingSingletonCommands.delete(commandType); }) .catch(() => null); const b = Buffer.alloc(1024); if (isPrimary) { b.writeUint8(0x03, 0); b.writeUint8(commandType, 1); } else { b.writeUint8(commandType, 0); } this.#socket.sendLegacyWrites([b]); // TODO - improve this timeout setTimeout(() => { command.reject(new Error('Timeout')); }, 5000); return command.promise; } async sendReports(buffers) { this.#socket.sendLegacyWrites(buffers); } #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.TcpLegacyHidDevice = TcpLegacyHidDevice; //# sourceMappingURL=legacy.js.map