UNPKG

companion-rgblink-openapi-connector

Version:

This is POC of connector between modules for Bitfocus Companion (https://bitfocus.io/companion) and RGBlink (https://www.rgblink.com/) devices managed by RGBLink OpenAPI

379 lines (378 loc) 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RGBLinkApiConnector = exports.ApiConfig = exports.SentCommandStorage = exports.SentCommand = exports.PollingCommand = void 0; const base_1 = require("@companion-module/base"); const MAX_COMMANDS_WAITING_FOR_RESPONSES_FOR_POLLING = 5; const COMMANDS_EXPIRE_TIME_SECONDS = 15; class PollingCommand { constructor(CMD, DAT1, DAT2, DAT3, DAT4) { if (CMD.length != 2 || DAT1.length != 2 || DAT2.length != 2 || DAT3.length != 2 || DAT4.length != 2) { console.log(`Bad command params: CMD:'${CMD}' DAT1:'${DAT1}' DAT2:'${DAT2}' DAT3:'${DAT3}' DAT4:'${DAT4}'`); } this.CMD = CMD; this.DAT1 = DAT1; this.DAT2 = DAT2; this.DAT3 = DAT3; this.DAT4 = DAT4; } } exports.PollingCommand = PollingCommand; class SentCommand { constructor(command, sentDate) { this.command = command; this.sentDate = sentDate; } } exports.SentCommand = SentCommand; class SentCommandStorage { constructor() { this.commandsSentWithoutResponse = []; this.expireTimeSeconds = COMMANDS_EXPIRE_TIME_SECONDS; } registerSentCommand(cmd) { //console.log('OUT ' + cmd) this.internalRememberCommand(cmd); } registerReceivedCommand(cmd) { //console.log('IN ' + cmd) this.internalCompareResponseWithSentCommands(cmd); } getCountElementsWithoutRespond() { return this.commandsSentWithoutResponse.length; } deleteExpiredCommands() { const currentMs = new Date().getTime(); let deleted = []; //for (let i = 0; i < this.commandsSentWithoutResponse.length; i++) { let i = this.commandsSentWithoutResponse.length; while (i--) { const sent = this.commandsSentWithoutResponse[i]; if (sent.sentDate + this.expireTimeSeconds * 1000 < currentMs) { deleted = deleted.concat(this.commandsSentWithoutResponse.splice(i, 1)); } } return deleted; } deleteAll() { this.commandsSentWithoutResponse = []; } setExpireTimeSecons(seconds) { this.expireTimeSeconds = seconds; } internalRememberCommand(cmd) { //console.log('Storing ' + cmd + '...') this.commandsSentWithoutResponse.push(new SentCommand(cmd, new Date().getTime())); } internalCompareResponseWithSentCommands(receivedCmd) { const receivedCmdId = this.internalGetCmdId(receivedCmd); let found = false; for (let i = 0; i < this.commandsSentWithoutResponse.length; i++) { const sent = this.commandsSentWithoutResponse[i]; const sentCmdId = this.internalGetCmdId(sent.command); if (receivedCmdId == sentCmdId) { //console.log('Found sent command for response ' + receivedCmd) found = true; this.commandsSentWithoutResponse.splice(i, 1); break; } } if (!found) { console.log('No sent command matching received response: ' + receivedCmd); } } internalGetCmdId(cmd) { return cmd.substr(2, 6); } } exports.SentCommandStorage = SentCommandStorage; class ApiConfig { constructor(host, port, polling, logEveryCommand) { this.host = host; this.port = port; this.polling = polling; this.logEveryCommand = logEveryCommand; } } exports.ApiConfig = ApiConfig; class RGBLinkApiConnector { constructor(config, pollingCommands) { this.PARSE_INT_HEX_MODE = 16; // eslint-disable-next-line @typescript-eslint/no-explicit-any this.eventsListeners = {}; this.nextSn = 0; // createTime = new Date().getTime() this.sentCommandStorage = new SentCommandStorage(); this.pollingQueue = []; this.pollingCommands = []; this.config = config; this.pollingCommands = pollingCommands; if (this.config && this.config.host) { this.createSocket(this.config.host, this.config.port); } // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; //this.intervalHandler100ms = setInterval(self.onEvery100Miliseconds, 100) this.intervalHandler100ms = setInterval(function () { self.onEvery100Miliseconds(); }, 100); } enableLog(logProvider) { this.logProvider = logProvider; } disableLog() { this.logProvider = undefined; } myDebug(msg) { this.internalMyLog('debug', msg); } myWarn(msg) { this.internalMyLog('warn', msg); } internalMyLog(level, msg) { try { if (this.logProvider) { this.logProvider.log(level, msg); } else { console.log(msg); } } catch (ex) { console.log(ex); // is it log anything? } } onEvery100Miliseconds() { this.doPolling(); } doPolling(force = false) { // send polling commands - which asks about device status // don't wait for more than 5 commands (rgblink requirements described near SN field in API specification) // remove commands with no response in 15 seconds const deleted = this.sentCommandStorage.deleteExpiredCommands(); if (deleted.length > 0) { deleted.forEach((sendCom) => { this.myWarn('Expired command (without response):' + sendCom.command); this.emit(RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_WARNING, 'The device did not respond to the command within ' + COMMANDS_EXPIRE_TIME_SECONDS + ' seconds'); }); } try { let commandsRequested = false; for (let i = 0; i < MAX_COMMANDS_WAITING_FOR_RESPONSES_FOR_POLLING; i++) { if (this.sentCommandStorage.getCountElementsWithoutRespond() >= 5) { // do not send more polling commands, if we wait for 5 or more responses // this.myDebug( // 'Skip more polling commands, current queue:' + // this.sentCommandStorage.getCountElementsWithoutRespond() + // ' added new ' + // i // ) break; } // if there is not polling commands, try to generate a new one, but only once if (this.pollingQueue.length == 0 && commandsRequested == false) { // Do NOT get new commands, if polling is disabled (hower, commands in queue will be send, this help to get device status after connect) if ((this.config && this.config.polling) || force) { this.pollingQueue = [...this.pollingCommands]; } commandsRequested = true; } if (this.pollingQueue.length == 0) { // if still no polling commands, stop doing anything break; } const command = this.pollingQueue.shift(); this.sendCommand(command.CMD, command.DAT1, command.DAT2, command.DAT3, command.DAT4); if (this.pollingQueue.length == 0 && commandsRequested == true) { // all sent, now more for sent, break break; } } } catch (ex) { console.log(ex); } } // readStatusAfterConnect() { // this.sentCommandStorage.deleteAll() // this.doPolling(true) // } onHostPortUpdate(host, port) { this.createSocket(host, port); } createSocket(host, port) { this.myDebug('RGBLinkApiConnector: creating socket ' + host + ':' + port + '...'); this.config.host = host; this.config.port = port; if (this.socket !== undefined) { this.socket.destroy(); delete this.socket; } if (host && port) { this.socket = new base_1.UDPHelper(host, port); this.socket.on('status_change', (status, message) => { this.myDebug('RGBLinkApiConnector: udp status_change:' + status + ' ' + message); }); this.socket.on('error', (err) => { this.myDebug('RGBLinkApiConnector: udp error:' + err); }); this.socket.on('data', (message) => { if (this.config && this.config.logEveryCommand) { this.myDebug('FEEDBACK: ' + message); } this.onDataReceived(message); }); // this.sendConnectMessage() // this.readStatusAfterConnect() } } onDataReceived(message) { try { if (message.length !== 19) { this.emit(RGBLinkApiConnector.EVENT_NAME_ON_DATA_API_NOT_STANDARD_LENGTH, [message]); } else { this.validateReceivedDataAndEmitIfValid(message); } } catch (ex) { console.log(ex); } } logFeedback(redeableMsg, info) { if (this.config && this.config.logEveryCommand) { this.myDebug(`Feedback: ${redeableMsg} ${info}`); } } onDestroy() { if (this.socket !== undefined) { this.socket.destroy(); } clearInterval(this.intervalHandler100ms); } // eslint-disable-next-line @typescript-eslint/no-explicit-any on(event, listener) { if (typeof this.eventsListeners[event] !== 'object') { this.eventsListeners[event] = []; } this.eventsListeners[event].push(listener); } // eslint-disable-next-line @typescript-eslint/no-explicit-any emit(event, args) { if (typeof this.eventsListeners[event] === 'object') { const listeners = this.eventsListeners[event].slice(); if (!Array.isArray(args)) { args = [args]; } for (let i = 0; i < listeners.length; i++) { listeners[i].apply(this, args); } } } sendCommandNative(cmd) { console.log('sendCommandNative ' + cmd); //let self = this try { if (cmd !== undefined && cmd != '') { if (this.socket !== undefined) { this.socket.send(cmd).then(function () { // self.myLog('sent?') }); if (this.config && this.config.logEveryCommand) { this.myDebug('SENT : ' + cmd); } this.sentCommandStorage.registerSentCommand(cmd); } else { this.myDebug("Can't send command, socket undefined!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } } } catch (ex) { console.log(ex); } } setPolling(polling) { this.config.polling = polling; } setLogEveryCommand(logEveryCommand) { this.config.logEveryCommand = logEveryCommand; } sendCommand(CMD, DAT1, DAT2, DAT3, DAT4) { const ADDR = '00'; this.sendCommandWithAddr(ADDR, CMD, DAT1, DAT2, DAT3, DAT4); } sendCommandWithAddr(ADDR, CMD, DAT1, DAT2, DAT3, DAT4) { const SN = this.byteToTwoSignHex(this.nextSn); this.incrementNextSn(); const checksum = this.calculateChecksum(ADDR, SN, CMD, DAT1, DAT2, DAT3, DAT4); const cmd = '<T' + ADDR + SN + CMD + DAT1 + DAT2 + DAT3 + DAT4 + checksum + '>'; this.sendCommandNative(cmd); } byteToTwoSignHex(b) { let out = parseInt(b).toString(this.PARSE_INT_HEX_MODE).toUpperCase(); while (out.length < 2) { out = '0' + out; } return out; } incrementNextSn() { this.nextSn++; if (this.nextSn > 255) { this.nextSn = 0; } } validateReceivedDataAndEmitIfValid(message) { const redeableMsg = message.toString('utf8').toUpperCase(); this.sentCommandStorage.registerReceivedCommand(redeableMsg); // Checksum checking const checksumInMessage = redeableMsg.substr(16, 2); const ADDR = redeableMsg.substr(2, 2); const SN = redeableMsg.substr(4, 2); const CMD = redeableMsg.substr(6, 2); const DAT1 = redeableMsg.substr(8, 2); const DAT2 = redeableMsg.substr(10, 2); const DAT3 = redeableMsg.substr(12, 2); const DAT4 = redeableMsg.substr(14, 2); const calculatedChecksum = this.calculateChecksum(ADDR, SN, CMD, DAT1, DAT2, DAT3, DAT4); if (checksumInMessage != calculatedChecksum) { this.emit(RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_WARNING, 'Incorrect checksum ' + redeableMsg); this.myDebug('redeableMsg Incorrect checksum: ' + checksumInMessage + ' != ' + calculatedChecksum); return; } if (redeableMsg[0] != '<' || redeableMsg[1] != 'F' || redeableMsg[18] != '>') { this.emit(RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_WARNING, 'Message is not a feedback:' + redeableMsg); return; } if (redeableMsg.includes('FFFFFFFF')) { this.emit(RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_WARNING, 'Feedback with error:' + redeableMsg); return; } // end of validate section this.emit(RGBLinkApiConnector.EVENT_NAME_ON_DATA_API, [ADDR, SN, CMD, DAT1, DAT2, DAT3, DAT4]); } //TODO make me private calculateChecksum(ADDR, SN, CMD, DAT1, DAT2, DAT3, DAT4) { let sum = 0; sum += parseInt(ADDR, this.PARSE_INT_HEX_MODE); sum += parseInt(SN, this.PARSE_INT_HEX_MODE); sum += parseInt(CMD, this.PARSE_INT_HEX_MODE); sum += parseInt(DAT1, this.PARSE_INT_HEX_MODE); sum += parseInt(DAT2, this.PARSE_INT_HEX_MODE); sum += parseInt(DAT3, this.PARSE_INT_HEX_MODE); sum += parseInt(DAT4, this.PARSE_INT_HEX_MODE); let checksum = (sum % 256).toString(this.PARSE_INT_HEX_MODE).toUpperCase(); while (checksum.length < 2) { checksum = '0' + checksum; } return checksum; } getNumberOfCommandsWithoutRespond() { return this.sentCommandStorage.getCountElementsWithoutRespond(); } } exports.RGBLinkApiConnector = RGBLinkApiConnector; RGBLinkApiConnector.EVENT_NAME_ON_DATA_API = 'on_data'; RGBLinkApiConnector.EVENT_NAME_ON_DATA_API_NOT_STANDARD_LENGTH = 'on_data_not_standard_length'; RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_OK = 'on_connection_ok'; RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_WARNING = 'on_connection_warning'; RGBLinkApiConnector.EVENT_NAME_ON_CONNECTION_ERROR = 'on_connection_error';