UNPKG

@elgato-stream-deck/tcp

Version:

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

283 lines 10.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SocketWrapper = exports.CoraMessageFlags = exports.CoraHidOp = void 0; const net_1 = require("net"); const EventEmitter = require("events"); const constants_js_1 = require("./constants.js"); var CoraHidOp; (function (CoraHidOp) { CoraHidOp[CoraHidOp["WRITE"] = 0] = "WRITE"; CoraHidOp[CoraHidOp["SEND_REPORT"] = 1] = "SEND_REPORT"; CoraHidOp[CoraHidOp["GET_REPORT"] = 2] = "GET_REPORT"; })(CoraHidOp || (exports.CoraHidOp = CoraHidOp = {})); var CoraMessageFlags; (function (CoraMessageFlags) { CoraMessageFlags[CoraMessageFlags["VERBATIM"] = 32768] = "VERBATIM"; CoraMessageFlags[CoraMessageFlags["REQ_ACK"] = 16384] = "REQ_ACK"; CoraMessageFlags[CoraMessageFlags["ACK_NAK"] = 512] = "ACK_NAK"; CoraMessageFlags[CoraMessageFlags["RESULT"] = 256] = "RESULT"; CoraMessageFlags[CoraMessageFlags["NONE"] = 0] = "NONE"; })(CoraMessageFlags || (exports.CoraMessageFlags = CoraMessageFlags = {})); class SocketWrapper extends EventEmitter { #socket; #address; #port; #connected = false; #retryConnectTimeout = null; #connectionActive = false; // True when connected/connecting/reconnecting #lastReceived = Date.now(); #receiveBuffer = null; #packetMode = 'unknown'; get isCora() { return this.#packetMode === 'cora'; } get isLegacy() { return this.#packetMode === 'legacy'; } constructor(host, port) { super(); this.#socket = new net_1.Socket(); this.#socket.on('error', (e) => { if (this.#connectionActive) { this.emit('error', 'socket error', e); } }); this.#socket.on('close', () => { if (this.#connected) this.emit('disconnected', this); this.#connected = false; // if (this._pingInterval) { // clearInterval(this._pingInterval) // this._pingInterval = null // } this._triggerRetryConnection(); }); this.#socket.on('data', (d) => this.#handleData(d)); this.#connectionActive = true; this.#address = host; this.#port = port || constants_js_1.DEFAULT_TCP_PORT; this.#socket.connect(this.#port, this.#address); } get connected() { return this.#connected; } get address() { return this.#address; } get port() { return this.#port; } checkForTimeout() { if (!this.#connectionActive) return; if (this.#retryConnectTimeout) return; if (this.#lastReceived + constants_js_1.TIMEOUT_DURATION < Date.now()) { this.#connected = false; setImmediate(() => this.emit('disconnected', this)); this._retryConnection(); } } _triggerRetryConnection() { if (!this.#retryConnectTimeout) { this.#retryConnectTimeout = setTimeout(() => { this._retryConnection(); }, constants_js_1.RECONNECT_INTERVAL); } } _retryConnection() { if (this.#retryConnectTimeout) { clearTimeout(this.#retryConnectTimeout); this.#retryConnectTimeout = null; } if (!this.connected && this.#connectionActive) { // Avoid timeouts while reconnecting this.#lastReceived = Date.now(); // Reset the packet mode, just in case this.#packetMode = 'unknown'; try { this.#socket.connect(this.#port, this.#address); } catch (e) { this._triggerRetryConnection(); this.emit('error', 'connection failed', e); // this._log('connection failed', e) console.log('connection failed', e); } } } #handleData(data) { this.#lastReceived = Date.now(); // If this is the first packet, check for the packet type if (this.#packetMode === 'unknown') { if (data.indexOf(constants_js_1.CORA_MAGIC) === 0) { this.#packetMode = 'cora'; } else if (data[0] === 1 && data[1] === 10) { // Check for SDS packet this.#packetMode = 'legacy'; } else { this.emit('error', 'Unknown packet type', new Error()); return; } } // Append data to buffer if (!this.#receiveBuffer || this.#receiveBuffer.length === 0) { this.#receiveBuffer = data; } else { this.#receiveBuffer = Buffer.concat([this.#receiveBuffer, data]); } switch (this.#packetMode) { case 'cora': this.#handleCoraDataPackets(); break; case 'legacy': this.#handleLegacyDataPackets(); break; default: this.emit('error', 'Unknown packet type', new Error()); break; } } #handleLegacyDataPackets() { if (!this.#receiveBuffer) return; // Pop and handle packets const PACKET_SIZE = 512; while (this.#receiveBuffer.length >= PACKET_SIZE) { const packet = this.#receiveBuffer.subarray(0, PACKET_SIZE); this.#receiveBuffer = this.#receiveBuffer.subarray(PACKET_SIZE); this.#handleLegacyDataPacket(packet); } // If buffer is empty, remove the reference if (this.#receiveBuffer.length === 0) { this.#receiveBuffer = null; } } #handleLegacyDataPacket(packet) { if (packet[0] === 1 && packet[1] === 10) { // Handle keepalive packet // Report as connected, if not already if (!this.#connected) { this.#connected = true; setImmediate(() => this.emit('connected', this)); } const ackBuffer = Buffer.alloc(1024); ackBuffer.writeUInt8(3, 0); ackBuffer.writeUInt8(26, 1); ackBuffer.writeUInt8(packet[5], 2); // connection no this.#socket.write(ackBuffer); } else { try { this.emit('dataLegacy', packet); } catch (e) { this.emit('error', 'Handle data error', e); } } } #handleCoraDataPackets() { if (!this.#receiveBuffer || this.#receiveBuffer.length < 16) return; // If the buffer doesn't start with the Cora magic bytes, search for the actual start of the packet const coraMagicIndex = this.#receiveBuffer.indexOf(constants_js_1.CORA_MAGIC); if (coraMagicIndex === -1) { // No Cora magic found, discard the buffer and wait for more data this.#receiveBuffer = this.#receiveBuffer.subarray(-4); // Keep the last 4 bytes, in case they are part of the next packet magic bytes return; } else if (coraMagicIndex > 0) { // If the magic is not at the start, slice the buffer to start from the magic this.#receiveBuffer = this.#receiveBuffer.subarray(coraMagicIndex); } // While there is a full header while (this.#receiveBuffer.length >= 16) { // Make sure we have the full payload const payloadLength = this.#receiveBuffer.readUint32LE(12); if (this.#receiveBuffer.length < 16 + payloadLength) return; const message = { flags: this.#receiveBuffer.readUint16LE(4), hidOp: this.#receiveBuffer.readUint8(6), messageId: this.#receiveBuffer.readUint32LE(8), payload: this.#receiveBuffer.subarray(16, 16 + payloadLength), }; // Pop the remaining content this.#receiveBuffer = this.#receiveBuffer.subarray(16 + payloadLength); // Handle the message this.#handleCoraDataPacket(message); } } #handleCoraDataPacket(packet) { if (packet.payload.length > 4 && packet.payload[0] === 1 && packet.payload[1] === 10) { // Handle keepalive packet // Report as connected, if not already if (!this.#connected) { this.#connected = true; setImmediate(() => this.emit('connected', this)); } const ackBuffer = Buffer.alloc(32); ackBuffer.writeUInt8(3, 0); ackBuffer.writeUInt8(26, 1); ackBuffer.writeUInt8(packet.payload[5], 2); // connection no // Send an ACK this.#sendCoraMessage({ flags: CoraMessageFlags.ACK_NAK, hidOp: packet.hidOp, messageId: packet.messageId, payload: ackBuffer, }); } else { try { this.emit('dataCora', packet); } catch (e) { this.emit('error', 'Handle data error', e); } } } async close() { try { this.#connectionActive = false; if (this.#retryConnectTimeout) { clearTimeout(this.#retryConnectTimeout); this.#retryConnectTimeout = null; } } finally { this.#socket.destroy(); } } #sendCoraMessage(message) { const buffer = Buffer.alloc(16); //+ message.payload.length) constants_js_1.CORA_MAGIC.copy(buffer, 0, 0, constants_js_1.CORA_MAGIC.length); buffer.writeUint16LE(message.flags, 4); buffer.writeUint8(message.hidOp, 6); buffer.writeUint32LE(message.messageId, 8); buffer.writeUint32LE(message.payload.length, 12); // buffer.set(message.payload, 16) // Avoid a copy by writing the payload directly to the socket this.#socket.write(buffer); this.#socket.write(message.payload); } sendLegacyWrites(buffers) { if (this.#packetMode !== 'legacy') throw new Error('sendLegacyWrites can only be used in legacy mode'); for (const buffer of buffers) { this.#socket.write(buffer); } } sendCoraWrites(messages) { if (this.#packetMode !== 'cora') throw new Error('sendCoraWrites can only be used in cora mode'); for (const message of messages) { this.#sendCoraMessage(message); } } } exports.SocketWrapper = SocketWrapper; //# sourceMappingURL=socketWrapper.js.map