UNPKG

@elgato-stream-deck/tcp

Version:

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

236 lines 9.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamDeckTcpConnectionManager = void 0; const events_1 = require("events"); const constants_js_1 = require("./constants.js"); const socketWrapper_js_1 = require("./socketWrapper.js"); const node_lib_1 = require("@elgato-stream-deck/node-lib"); const core_1 = require("@elgato-stream-deck/core"); const tcpWrapper_js_1 = require("./tcpWrapper.js"); const legacy_js_1 = require("./hid-device/legacy.js"); const cora_js_1 = require("./hid-device/cora.js"); class StreamDeckTcpConnectionManager extends events_1.EventEmitter { #connections = new Map(); #streamdecks = new Map(); #openOptions; #autoConnectToSecondaries; #timeoutInterval = null; constructor(userOptions) { super(); // Clone the options, to ensure they dont get changed const jpegOptions = userOptions?.jpegOptions ? { ...userOptions.jpegOptions } : undefined; this.#openOptions = { encodeJPEG: async (buffer, width, height) => (0, node_lib_1.encodeJPEG)(buffer, width, height, jpegOptions), ...userOptions, }; this.#autoConnectToSecondaries = userOptions?.autoConnectToSecondaries ?? true; } #getConnectionId(address, port) { return `${address}:${port || constants_js_1.DEFAULT_TCP_PORT}`; } #onSocketConnected = (socket) => { const connectionId = this.#getConnectionId(socket.address, socket.port); const fakeHidDevice = socket.isCora ? new cora_js_1.TcpCoraHidDevice(socket) : new legacy_js_1.TcpLegacyHidDevice(socket); // Setup a temporary error handler, in case an error gets produced during the setup const tmpErrorHandler = () => { // No-op? }; fakeHidDevice.on('error', tmpErrorHandler); fakeHidDevice .getDeviceInfo() .then((info) => { const model = core_1.DEVICE_MODELS.find((m) => m.productIds.includes(info.productId)); if (!model) { // Note: leave the temporary error handler, to ensure it can't cause a crash this.emit('error', `Found StreamDeck with unknown productId: ${info.productId.toString(16)}`); return; } const propertiesService = fakeHidDevice.isPrimary ? new TcpPropertiesService(fakeHidDevice) : undefined; const streamdeckSocket = model.factory(fakeHidDevice, this.#openOptions, propertiesService); const streamDeckTcp = new tcpWrapper_js_1.StreamDeckTcpWrapper(socket, fakeHidDevice, streamdeckSocket); fakeHidDevice.off('error', tmpErrorHandler); this.#streamdecks.set(connectionId, streamDeckTcp); setImmediate(() => this.emit('connected', streamDeckTcp)); if (this.#autoConnectToSecondaries && fakeHidDevice.isPrimary) { this.#tryConnectingToSecondary(connectionId, socket, streamDeckTcp); } }) .catch((err) => { this.emit('error', `Failed to open device ${connectionId}: ${err}`); }); }; #tryConnectingToSecondary(parentId, _parentSocket, parent) { const connectToUpdatedChildInfo = (info) => { // Check the current parent is still active const currentParent = this.#streamdecks.get(parentId); if (currentParent !== parent) return; // Get the parent socket info, this should always exist const parentSocketInfo = this.#connections.get(parentId); if (!parentSocketInfo) return; if (!info) { // Child disconnected if (parentSocketInfo.childId) { this.#disconnectFromId(parentSocketInfo.childId); parentSocketInfo.childId = null; } } else { const childId = this.#getConnectionId(parent.remoteAddress, info.tcpPort); if (childId === parentId) return; // Shouldn't happen, but could cause an infinite loop if (parentSocketInfo.childId !== childId || !this.#connections.has(childId)) { // Make sure an existing child is disposed if (parentSocketInfo.childId) { this.#disconnectFromId(parentSocketInfo.childId); } // Start connecting to the new child parentSocketInfo.childId = childId; this.#connectToInternal(parent.remoteAddress, info.tcpPort, {}); } } }; // Setup watching hotplug events parent.tcpEvents.on('childChange', (info) => connectToUpdatedChildInfo(info)); // Do a check now, to see what is connected parent .getChildDeviceInfo() .then((childInfo) => connectToUpdatedChildInfo(childInfo)) .catch(() => { // TODO - log }); } #onSocketDisconnected = (socket) => { const id = this.#getConnectionId(socket.address, socket.port); // Clear and re-add all listeners, to ensure we don't leak anything socket.removeAllListeners(); this.#setupSocketEventHandlers(socket); const streamdeck = this.#streamdecks.get(id); if (streamdeck) { this.#streamdecks.delete(id); setImmediate(() => this.emit('disconnected', streamdeck)); } }; #startTimeoutInterval() { if (this.#timeoutInterval) return; this.#timeoutInterval = setInterval(() => { for (const entry of this.#connections.values()) { entry.socket.checkForTimeout(); } }, 1000); } #stopTimeoutInterval() { if (!this.#timeoutInterval) return; if (this.#connections.size > 0) return; clearInterval(this.#timeoutInterval); this.#timeoutInterval = null; } connectTo(address, port = constants_js_1.DEFAULT_TCP_PORT, options) { if (!this.#connectToInternal(address, port, options)) { throw new Error('Connection already exists'); } } #connectToInternal(address, port, _options) { const id = this.#getConnectionId(address, port); if (this.#connections.has(id)) return false; const newSocket = new socketWrapper_js_1.SocketWrapper(address, port); this.#setupSocketEventHandlers(newSocket); this.#connections.set(id, { socket: newSocket, childId: null }); this.#startTimeoutInterval(); return true; } #setupSocketEventHandlers(socket) { socket.on('connected', () => this.#onSocketConnected(socket)); socket.on('disconnected', () => this.#onSocketDisconnected(socket)); socket.on('error', () => { // TODO }); } disconnectFrom(address, port = constants_js_1.DEFAULT_TCP_PORT) { const id = this.#getConnectionId(address, port); return this.#disconnectFromId(id); } #disconnectFromId(id) { const entry = this.#connections.get(id); if (!entry) return false; // Disconnect from child if it is known if (entry.childId) { this.#disconnectFromId(entry.childId); } this.#connections.delete(id); entry.socket.close().catch(() => { // TODO - log }); this.#stopTimeoutInterval(); return true; } disconnectFromAll() { for (const socket of this.#connections.values()) { socket.socket.close().catch(() => { // TODO - log }); } this.#connections.clear(); } getStreamdeckFor(address, port = constants_js_1.DEFAULT_TCP_PORT) { return this.#streamdecks.get(this.#getConnectionId(address, port)); } } exports.StreamDeckTcpConnectionManager = StreamDeckTcpConnectionManager; class TcpPropertiesService { #device; constructor(device) { this.#device = device; } async setBrightness(percentage) { if (percentage < 0 || percentage > 100) { throw new RangeError('Expected brightness percentage to be between 0 and 100'); } const buffer = Buffer.alloc(1024); buffer.writeUint8(0x03, 0); buffer.writeUint8(0x08, 1); buffer.writeUint8(percentage, 2); await this.#device.sendFeatureReport(buffer); } async resetToLogo() { throw new Error('Not implemented'); // TODO the soft reset below is too much, needs something lighter // const buffer = Buffer.alloc(1024) // buffer.writeUint8(0x03, 0) // buffer.writeUint8(0x0b, 1) // buffer.writeUint8(0, 2) // Soft Reset // await this.#sendMessages([buffer]) } async getFirmwareVersion() { const data = await this.#device.getFeatureReport(0x83, -1); return new TextDecoder('ascii').decode(data.subarray(8, 16)); } async getAllFirmwareVersions() { const [ap2Data, encoderAp2Data, encoderLdData] = await Promise.all([ this.#device.getFeatureReport(0x83, -1), this.#device.getFeatureReport(0x86, -1), this.#device.getFeatureReport(0x8a, -1), ]); return (0, core_1.parseAllFirmwareVersionsHelper)({ ap2: ap2Data.slice(2), encoderAp2: encoderAp2Data.slice(2), encoderLd: encoderLdData.slice(2), }); } async getSerialNumber() { const data = await this.#device.getFeatureReport(0x84, -1); const length = data[3]; return new TextDecoder('ascii').decode(data.subarray(4, 4 + length)); } } //# sourceMappingURL=connectionManager.js.map