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