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