UNPKG

timeline-state-resolver

Version:
634 lines • 22.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Pharos = exports.Protocol = void 0; const WebSocket = require("ws"); const events_1 = require("events"); const got_1 = require("got"); const _ = require("underscore"); const CONNECT_TIMEOUT = 3000; const PING_TIMEOUT = 10 * 1000; var Protocol; (function (Protocol) { Protocol["DMX"] = "dmx"; Protocol["PATHPORT"] = "pathport"; Protocol["ARTNET"] = "art-net"; Protocol["KINET"] = "kinet"; Protocol["SACN"] = "sacn"; Protocol["DVI"] = "dvi"; Protocol["RIODMX"] = "rio-dmx"; })(Protocol = exports.Protocol || (exports.Protocol = {})); /** * Implementation of the Pharos V2 http API */ class Pharos extends events_1.EventEmitter { constructor() { super(...arguments); this._socket = null; this._keepAlive = false; this._replyReceived = false; this._queryString = ''; this._serverSessionKey = null; this._reconnectAttempts = 0; this._isConnecting = false; this._isReconnecting = false; this._aboutToReconnect = false; this._pendingMessages = []; this._requestPromises = {}; this._broadcastCallbacks = {}; this._connected = false; this._webSocketKeepAliveTimeout = null; } // constructor () {} async connect(options) { this._isConnecting = true; this._options = options; return this._connectSocket().then(() => { this._isConnecting = false; }); } get connected() { return this._connected; } async dispose() { return new Promise((resolve) => { _.each(this._requestPromises, (rp, id) => { _.each(rp, (promise) => { promise.reject('Disposing'); }); delete this._requestPromises[id]; }); _.each(this._broadcastCallbacks, (_fcns, id) => { delete this._broadcastCallbacks[id]; }); if (this.connected) { this.once('disconnected', resolve); } if (this._socket) this._socket.close(); if (!this.connected) { resolve(); } }); } async getSystemInfo() { return this.request('system'); } async getProjectInfo() { return this.request('project'); } async getCurrentTime() { return this.request('time'); } /** * @param params Example: { num: '1,2,5-9' } */ async getTimelineInfo(num) { return this.getThingInfo('timeline', num); } /** * @param params Example: { num: '1,2,5-9' } */ async getSceneInfo(num) { const params = {}; if (num) params.num = num + ''; return this.getThingInfo('scene', num); } /** * @param params Example: { num: '1,2,5-9' } */ async getGroupInfo(num) { return this.getThingInfo('group', num); } async getThingInfo(thing, num) { const params = {}; if (num) params.num = num + ''; return this.request(thing, params); } async getContentTargetInfo() { return this.request('content_target'); } async getControllerInfo() { return this.request('controller'); } async getRemoteDeviceInfo() { return this.request('remote_device'); } async getTemperature() { return this.request('temperature'); } async getFanSpeed() { return this.request('fan_speed'); } async getTextSlot(names) { const params = {}; if (names) { if (!_.isArray(names)) names = [names]; params.names = names.join(','); // TODO: test that this actually works } return this.request('text_slot', params); } async getProtocols() { return this.request('protocol'); } /** * @param key {universe?: universeKey} Example: "dmx:1", "rio-dmx:rio44:1" // DMX, Pathport, sACN and Art-Net, protocol:kinetPowerSupplyNum:kinetPort for KiNET and protocol:remoteDeviceType:remoteDeviceNum for RIO DMX */ async getOutput(universe) { const params = {}; if (universe) params.universe = universe; return this.request('output', params); } async getLuaVariables(vars) { const params = {}; if (vars) { if (!_.isArray(vars)) vars = [vars]; params.variables = vars.join(','); } return this.request('lua', params); } async getTriggers() { return this.request('trigger'); } async subscribeTimelineStatus(callback) { return this.subscribe('timeline', callback); } async subscribeSceneStatus(callback) { return this.subscribe('scene', callback); } async subscribeGroupStatus(callback) { return this.subscribe('group', callback); } async subscribeContentTargetStatus(callback) { return this.subscribe('content_target', callback); } async subscribeRemoteDeviceStatus(callback) { return this.subscribe('remote_device', callback); } async subscribeBeacon(callback) { return this.subscribe('beacon', callback); } async subscribeLua(callback) { return this.subscribe('lua', callback); } async startTimeline(timelineNum) { return this.command('POST', '/api/timeline', { action: 'start', num: timelineNum }); } async startScene(sceneNum) { return this.command('POST', '/api/scene', { action: 'start', num: sceneNum }); } async releaseTimeline(timelineNum, fade) { return this.command('POST', '/api/timeline', { action: 'release', num: timelineNum, fade: fade }); } async releaseScene(sceneNum, fade) { return this.command('POST', '/api/scene', { action: 'release', num: sceneNum, fade: fade }); } async toggleTimeline(timelineNum, fade) { return this.command('POST', '/api/timeline', { action: 'toggle', num: timelineNum, fade: fade }); } async toggleScene(sceneNum, fade) { return this.command('POST', '/api/scene', { action: 'toggle', num: sceneNum, fade: fade }); } async pauseTimeline(timelineNum) { return this.command('POST', '/api/timeline', { action: 'pause', num: timelineNum }); } async resumeTimeline(timelineNum) { return this.command('POST', '/api/timeline', { action: 'resume', num: timelineNum }); } async pauseAll() { return this.command('POST', '/api/timeline', { action: 'pause' }); } async resumeAll() { return this.command('POST', '/api/timeline', { action: 'resume' }); } async releaseAllTimelines(group, fade) { return this.command('POST', '/api/timeline', { action: 'release', group: group, fade: fade }); } async releaseAllScenes(group, fade) { return this.command('POST', '/api/scene', { action: 'release', group: group, fade: fade }); } async releaseAll(group, fade) { return this.command('POST', '/api/release_all', { group: group, fade: fade }); } async setTimelineRate(timelineNum, rate) { return this.command('POST', '/api/timeline', { action: 'set_rate', num: timelineNum, rate: rate }); } async setTimelinePosition(timelineNum, position) { return this.command('POST', '/api/timeline', { action: 'set_position', num: timelineNum, position: position }); } async fireTrigger(triggerNum, vars, testConditions) { return this.command('POST', '/api/trigger', { num: triggerNum, var: (vars || []).join(','), conditions: !!testConditions, }); } async runCommand(input) { return this.command('POST', '/api/cmdline', { input: input, }); } /** * Master the intensity of a group (applied as a multiplier to output levels) * @param groupNum * @param level integer * @param fade float * @param delay float */ async masterIntensity(groupNum, level, fade, delay) { return this.command('POST', '/api/group', { action: 'master_intensity', num: groupNum, level: level, fade: fade, delay: delay, }); } /** * VLC/VLC +: Master the intensity of a content target (applied as a multiplier to output levels) * @param type type - of content target, 'primary', 'secondary', 'overlay_1', 'overlay_2'... * @param level integer * @param fade float * @param delay float */ async masterContentTargetIntensity(type, level, fade, delay) { return this.command('POST', '/api/content_target', { action: 'master_intensity', type: type, level: level, fade: fade, delay: delay, }); } async setGroupOverride(groupNum, options) { const params = _.extend({}, options, { num: groupNum, target: 'group', }); return this.command('PUT', '/api/override', params); } async setFixtureOverride(fixtureNum, options) { const params = _.extend({}, options, { num: fixtureNum, target: 'fixture', }); return this.command('PUT', '/api/override', params); } async clearGroupOverrides(groupNum, fade) { const params = { target: 'group', }; if (groupNum !== undefined) params.num = groupNum; if (fade !== undefined) params.fade = fade; return this.command('DELETE', '/api/override', params); } async clearFixtureOverrides(fixtureNum, fade) { const params = { target: 'fixture', }; if (fixtureNum !== undefined) params.num = fixtureNum; if (fade !== undefined) params.fade = fade; return this.command('DELETE', '/api/override', params); } async clearAllOverrides(fade) { const params = {}; if (fade !== undefined) params.fade = fade; return this.command('DELETE', '/api/override', params); } async enableOutput(protocol) { return this.command('POST', '/api/output', { action: 'enable', protocol: protocol }); } async disableOutput(protocol) { return this.command('POST', '/api/output', { action: 'disable', protocol: protocol }); } async setTextSlot(slot, value) { return this.command('PUT', '/api/text_slot', { name: slot, value: value, }); } async flashBeacon() { return this.command('POST', '/api/beacon'); } async parkChannel(universeKey, channelList, level) { return this.command('POST', '/api/channel', { universe: universeKey, channels: (channelList || []).join(','), level: level, }); } async unparkChannel(universeKey, channelList) { return this.command('DELETE', '/api/channel', { universe: universeKey, channels: (channelList || []).join(','), }); } async getLog() { return this.command('GET', '/api/log'); } async clearLog() { return this.command('DELETE', '/api/log'); } /** * power reboot */ async resetHardware() { return this.command('POST', '/api/reset'); } setInternalPage(isInternal) { this._queryString = isInternal ? '?internal_page' : ''; } async request(id, params) { const p = new Promise((resolve, reject) => { if (!this._requestPromises[id]) this._requestPromises[id] = []; this._requestPromises[id].push({ resolve, reject }); const json = { request: id }; if (params) { for (const name in params) { json[name] = params[name]; } } this._sendMessage(JSON.stringify(json)).catch((e) => { reject(e); }); }); return p; } async subscribe(id, callback) { if (!this._broadcastCallbacks[id]) this._broadcastCallbacks[id] = []; this._broadcastCallbacks[id].push(callback); const json = { subscribe: id }; return this._sendMessage(JSON.stringify(json)).then(() => { return; }); } async command(method, url0, data0) { return new Promise((resolve, reject) => { const url = `${this._options.ssl ? 'https' : 'http'}://${this._options.host}${url0}${this._queryString}`; const data = {}; if (data0) { _.each(data0, (value, key) => { if (value !== undefined && value !== null) { data[key] = value; } }); } let httpReq; switch (method) { case 'POST': httpReq = got_1.default.post; break; case 'PUT': httpReq = got_1.default.put; break; case 'GET': httpReq = got_1.default.get; break; case 'DELETE': httpReq = got_1.default.delete; break; default: reject(`Unknown method: "${method}"`); return; } httpReq(url, { json: data }) .then((response) => { if (response.statusCode === 400) { reject(new Error(`Error: [400]: Bad request`)); // TODO: Maybe handle other response-codes? } else if (response.statusCode >= 200 && response.statusCode <= 299) { resolve(response.body); } else { reject(new Error(`Error: StatusCode: [${response.statusCode}]`)); } }) .catch((error) => { this.emit('error', new Error(`Error ${method}: ${error}`)); reject(error); }); }); } async _connectSocket() { return new Promise((resolve, reject) => { const pathName = `${this._options.ssl ? 'wss:' : 'ws:'}//${this._options.host}/query${this._queryString}`; this._socket = new WebSocket(pathName); this._socket.binaryType = 'arraybuffer'; this.once('connected', () => { resolve(); }); this.once('error', (e) => { reject(e); }); setTimeout(() => { reject(new Error('Connection timeout')); }, CONNECT_TIMEOUT); this._socket.on('open', () => { this._connectionChanged(true); this._reconnectAttempts = 0; // reset reconnection attempts if (this._socket) { while (this._pendingMessages.length) { const m = this._pendingMessages.shift(); if (m) { this._socket.send(m.msg, (err) => { if (m) { if (err) m.reject(err); else m.resolve(); } }); } } } this._keepAlive = true; this._replyReceived = true; this._webSocketKeepAlive(); }); this._socket.on('message', (data, isBinary) => { // let data: WebSocket.Data = ev.data this._replyReceived = true; if (isBinary) { // @ts-ignore data type const array = new Int32Array(data); if (this._serverSessionKey) { // need to compare primitives as two objects are never the same if (this._serverSessionKey[0] !== array[0] || this._serverSessionKey[1] !== array[1] || this._serverSessionKey[2] !== array[2] || this._serverSessionKey[3] !== array[3]) { this.emit('restart'); this._serverSessionKey = array; } } else { this._serverSessionKey = array; } } else { const json = JSON.parse(data.toString()); this._onReceiveMessage(json); } }); this._socket.on('error', (e) => { this._handleWebsocketReconnection(e); }); this._socket.on('close', () => { // this._connectionChanged(false) this._handleWebsocketReconnection(); }); }); } async _sendMessage(msg) { return new Promise((resolve, reject) => { if (this._socket && this._socket.readyState === this._socket.OPEN) { this._socket.send(msg, (err) => { if (err) reject(err); else resolve(); }); } else { this._pendingMessages.push({ msg: msg, resolve: resolve, reject: reject, }); if (!this._socket || this._socket.readyState !== this._socket.CONNECTING) { this._connectSocket().catch((err) => { this.emit('error', err); }); } } }); } _webSocketKeepAlive() { // send a zero length message as a ping to keep the connection alive if (this._webSocketKeepAliveTimeout) { clearTimeout(this._webSocketKeepAliveTimeout); // to prevent multiple loops of pings } this._webSocketKeepAliveTimeout = null; if (this._keepAlive) { if (this._replyReceived) { if (this._connected) { // we only have to ping if we think we're connected this._sendMessage('').catch((e) => this.emit('error', e)); } this._webSocketKeepAliveTimeout = setTimeout(() => { this._webSocketKeepAlive(); }, PING_TIMEOUT); this._replyReceived = false; } else { // never got a reply, throw an error this._handleWebsocketReconnection(new Error('ping timeout')); } } } _reconnect() { if (this._isConnecting) return; // don't reconnect while a connect is already running if (!this._isReconnecting) { // try to _reconnect this._reconnectAttempts++; this._isReconnecting = true; this._connectSocket() .then(() => { this._isReconnecting = false; }) .catch((e) => { this._isReconnecting = false; this.emit('error', e); // If the reconnection failed and another reconnection attempt was ignored, do that now instead: if (this._aboutToReconnect) { this._aboutToReconnect = false; this._reconnect(); } }); } else { // Nothing, ignore if we're already trying to reconnect this._aboutToReconnect = true; } } _onReceiveMessage(json) { if (json.broadcast) { const bc = this._broadcastCallbacks[json.broadcast]; if (bc) { if (bc.length) { _.each(bc, (fcn) => { fcn(json.data); }); } else { this.emit('error', new Error(`no broadcastCallbacks found for ${json.broadcast}`)); } } else { this.emit('error', new Error(`no broadcastCallbacks array found for ${json.broadcast}`)); } } else if (json.request) { const rp = this._requestPromises[json.request]; if (rp) { const p = rp.shift(); if (p) { p.resolve(json.data); } else { this.emit('error', new Error(`no requestPromise found for ${json.request}`)); } } else { this.emit('error', new Error(`no requestPromise array found for ${json.request}`)); } } else if (json.redirect) { this.emit('error', `Redirect to ${json.redirect}`); } else { this.emit('error', `Unknown reply: ${json}`); } } _handleWebsocketReconnection(e) { // Called when a socket connection is closed for whatever reason this._keepAlive = false; this._socket = null; this._connectionChanged(false); if (e) { if (this._reconnectAttempts === 0) { // Only emit error on first error this.emit('error', e); } } setTimeout(() => { this._reconnect(); }, Math.min(60, this._reconnectAttempts) * 1000); } _connectionChanged(connected) { if (this._connected !== connected) { this._connected = connected; if (connected) { this.emit('connected'); } else { this.emit('disconnected'); } } } } exports.Pharos = Pharos; //# sourceMappingURL=connection.js.map