mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
V2 integration (#293)
* feat: integrations and lifecycles * refactor: prevent issues with start order
This commit is contained in:
+77
-50
@@ -6,11 +6,11 @@ import cors from 'cors';
|
||||
// import utils
|
||||
import { join, resolve } from 'path';
|
||||
|
||||
import { config } from './config/config.js';
|
||||
import { initiateOSC, shutdownOSCServer } from './controllers/OscController.js';
|
||||
import { initSentry } from './modules/sentry.js';
|
||||
import { currentDirectory, environment, isProduction, resolvedPath, uiPath } from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
// Import Routes
|
||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||
@@ -22,12 +22,9 @@ import { router as playbackRouter } from './routes/playbackRouter.js';
|
||||
import { DataProvider } from './classes/data-provider/DataProvider.js';
|
||||
import { socketProvider } from './classes/socket/SocketController.js';
|
||||
import { eventTimer } from './services/TimerService.js';
|
||||
import { promise } from './modules/loadDb.js';
|
||||
import { TimerType } from 'ontime-types';
|
||||
|
||||
// TODO: apply code and remove, refs PR #290
|
||||
const sharedType: TimerType = TimerType.CountDown;
|
||||
console.log('WIP', sharedType);
|
||||
import { dbLoadingProcess } from './modules/loadDb.js';
|
||||
import { integrationService } from './services/integration-service/IntegrationService.js';
|
||||
import { OscIntegration } from './services/integration-service/OscIntegration.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -77,6 +74,7 @@ app.use((error, response) => {
|
||||
});
|
||||
|
||||
/*************** START SERVICES ***************/
|
||||
|
||||
/* Override config
|
||||
* ----------------
|
||||
*
|
||||
@@ -84,42 +82,35 @@ app.use((error, response) => {
|
||||
* It can be overridden here by the settings in the db
|
||||
* It can also be overridden on call
|
||||
*
|
||||
* Start order
|
||||
* ----------------
|
||||
*
|
||||
* The services need to be started in a certain order,
|
||||
* the enum below enforces that
|
||||
*/
|
||||
(async () => {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
enum OntimeStartOrder {
|
||||
Error,
|
||||
InitDB,
|
||||
InitServer,
|
||||
InitIO,
|
||||
}
|
||||
|
||||
let step = OntimeStartOrder.InitDB;
|
||||
const checkStart = (currentState) => {
|
||||
if (step !== currentState) {
|
||||
step = OntimeStartOrder.Error;
|
||||
throw new Error('Init order error: startDb > startServer > startOsc > startIntegrations');
|
||||
} else {
|
||||
if (step === 1 || step === 2) {
|
||||
step = step + 1;
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const { osc } = DataProvider.getData();
|
||||
const oscIP = osc?.targetIP || config.osc.targetIP;
|
||||
const oscOutPort = osc?.portOut || config.osc.portOut;
|
||||
const oscInPort = osc?.port || config.osc.port;
|
||||
const oscInEnabled = osc?.enabled !== undefined ? osc.enabled : config.osc.inputEnabled;
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
|
||||
/**
|
||||
* @description starts OSC server
|
||||
* @description starts OSC server
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const startOSCServer = async (overrideConfig = null) => {
|
||||
if (!oscInEnabled) {
|
||||
socketServer.info('RX', 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup default port
|
||||
const oscSettings = {
|
||||
port: overrideConfig?.port || oscInPort,
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
socketServer.info('RX', `Starting OSC Server on port: ${oscInPort}`);
|
||||
initiateOSC(oscSettings);
|
||||
export const startDb = async () => {
|
||||
checkStart(OntimeStartOrder.InitDB);
|
||||
await dbLoadingProcess;
|
||||
};
|
||||
|
||||
// create HTTP server
|
||||
@@ -130,32 +121,67 @@ const expressServer = http.createServer(app);
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
export const startServer = async () => {
|
||||
// Start server
|
||||
checkStart(OntimeStartOrder.InitServer);
|
||||
|
||||
const serverPort = 4001; // hardcoded for now
|
||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||
expressServer.listen(serverPort, '0.0.0.0');
|
||||
|
||||
// init socket controller
|
||||
await socketServer.initServer(expressServer);
|
||||
socketServer.initServer(expressServer);
|
||||
socketServer.info('SERVER', 'Socket initialised');
|
||||
|
||||
socketServer.info('SERVER', returnMessage);
|
||||
socketServer.startListener();
|
||||
|
||||
return returnMessage;
|
||||
};
|
||||
|
||||
/**
|
||||
* starts integrations
|
||||
* @description starts OSC server
|
||||
* @description starts OSC server
|
||||
* @param overrideConfig
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
export const startIntegrations = async (overrideConfig = null) => {
|
||||
const { http } = DataProvider.getData();
|
||||
export const startOSCServer = async (overrideConfig = null) => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
// OSC Config
|
||||
const oscConfig = {
|
||||
ip: oscIP,
|
||||
port: overrideConfig?.port || oscOutPort,
|
||||
const { osc } = DataProvider.getData();
|
||||
|
||||
if (!osc.enabledIn) {
|
||||
socketServer.info('RX', 'OSC Input Disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup default port
|
||||
const oscSettings = {
|
||||
...osc,
|
||||
portIn: overrideConfig?.port || osc.portIn,
|
||||
};
|
||||
|
||||
// Start OSC Server
|
||||
socketServer.info('RX', `Starting OSC Server on port: ${oscSettings.portIn}`);
|
||||
initiateOSC(oscSettings);
|
||||
};
|
||||
|
||||
/**
|
||||
* starts integrations
|
||||
*/
|
||||
export const startIntegrations = async (config?: { osc: OSCSettings }) => {
|
||||
checkStart(OntimeStartOrder.InitIO);
|
||||
|
||||
const { osc } = config ?? DataProvider.getData();
|
||||
|
||||
if (!osc) {
|
||||
return 'OSC Invalid configuration';
|
||||
}
|
||||
|
||||
const oscIntegration = new OscIntegration();
|
||||
const { success, message } = oscIntegration.init(osc);
|
||||
socketServer.info('RX', message);
|
||||
|
||||
if (success) {
|
||||
integrationService.register(oscIntegration);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -170,6 +196,7 @@ export const shutdown = async (exitCode = 0) => {
|
||||
shutdownOSCServer();
|
||||
eventTimer.shutdown();
|
||||
socketServer.shutdown();
|
||||
integrationService.shutdown();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
|
||||
|
||||
+5
-33
@@ -3,6 +3,7 @@
|
||||
* and adds logic specific to ontime data
|
||||
*/
|
||||
import { data, db } from '../../modules/loadDb.js';
|
||||
import { safeMerge } from './DataProvider.utils.js';
|
||||
|
||||
export class DataProvider {
|
||||
static getData() {
|
||||
@@ -39,6 +40,7 @@ export class DataProvider {
|
||||
}
|
||||
|
||||
static async deleteEvent(eventId) {
|
||||
// @ts-expect-error -- this will go away once we type db
|
||||
data.rundown = Array.from(data.rundown).filter((e) => e.id !== eventId);
|
||||
await this.persist();
|
||||
}
|
||||
@@ -49,6 +51,7 @@ export class DataProvider {
|
||||
|
||||
static async clearRundown() {
|
||||
data.rundown = [];
|
||||
// @ts-expect-error -- not sure how to type, this is library side
|
||||
await db.write();
|
||||
}
|
||||
|
||||
@@ -149,11 +152,12 @@ export class DataProvider {
|
||||
}
|
||||
|
||||
static async persist() {
|
||||
// @ts-expect-error -- not sure how to type, this is library side
|
||||
await db.write();
|
||||
}
|
||||
|
||||
static async mergeIntoData(newData) {
|
||||
const mergedData = DataProvider.safeMerge(data, newData);
|
||||
const mergedData = safeMerge(data, newData);
|
||||
data.event = mergedData.event;
|
||||
data.settings = mergedData.settings;
|
||||
data.osc = mergedData.osc;
|
||||
@@ -163,36 +167,4 @@ export class DataProvider {
|
||||
data.rundown = mergedData.rundown;
|
||||
await this.persist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two data objects
|
||||
* @param {object} existing
|
||||
* @param {object} newData
|
||||
*/
|
||||
static safeMerge(existing, newData) {
|
||||
const mergedData = { ...existing };
|
||||
|
||||
if (typeof newData?.rundown !== 'undefined') {
|
||||
mergedData.rundown = newData.rundown;
|
||||
}
|
||||
if (typeof newData?.event !== 'undefined') {
|
||||
mergedData.event = { ...newData.event };
|
||||
}
|
||||
if (typeof newData?.settings !== 'undefined') {
|
||||
mergedData.settings = { ...newData.settings };
|
||||
}
|
||||
if (typeof newData?.osc !== 'undefined') {
|
||||
mergedData.osc = { ...newData.osc };
|
||||
}
|
||||
if (typeof newData?.http !== 'undefined') {
|
||||
mergedData.http = { ...newData.http };
|
||||
}
|
||||
if (typeof newData?.aliases !== 'undefined') {
|
||||
mergedData.aliases = [...newData.aliases];
|
||||
}
|
||||
if (typeof newData?.userFields !== 'undefined') {
|
||||
mergedData.userFields = { ...existing.userFields, ...newData.userFields };
|
||||
}
|
||||
return mergedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Merges two data objects
|
||||
* @param {object} existing
|
||||
* @param {object} newData
|
||||
*/
|
||||
export function safeMerge(existing, newData) {
|
||||
const { rundown, event, settings, osc, http, aliases, userFields } = newData || {};
|
||||
return {
|
||||
...existing,
|
||||
rundown: rundown ?? existing.rundown,
|
||||
event: { ...existing.event, ...event },
|
||||
settings: { ...existing.settings, ...settings },
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: aliases ?? existing.aliases,
|
||||
userFields: {
|
||||
...existing.userFields,
|
||||
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
|
||||
},
|
||||
osc: {
|
||||
...existing.osc,
|
||||
...osc,
|
||||
subscriptions: {
|
||||
...existing.osc?.subscriptions,
|
||||
...(newData?.osc?.subscriptions || {}),
|
||||
...(existing.osc?.subscriptions && newData?.osc?.subscriptions
|
||||
? Object.keys(existing.osc.subscriptions).reduce((acc, key) => {
|
||||
if (!(key in newData.osc.subscriptions)) {
|
||||
acc[key] = existing.osc.subscriptions[key];
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
http: { ...existing.http, ...http },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import { safeMerge } from '../DataProvider.utils.js';
|
||||
|
||||
describe('safeMerge', () => {
|
||||
const existing = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: 'existing title',
|
||||
publicUrl: 'existing public URL',
|
||||
backstageUrl: 'existing backstageUrl',
|
||||
backstageInfo: 'existing backstageInfo',
|
||||
endMessage: 'existing endMessage',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
lock: true,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
version: 2,
|
||||
serverPort: 4001,
|
||||
},
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
user0: 'existing user0',
|
||||
user1: 'existing user1',
|
||||
},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
enabled: true,
|
||||
user: null,
|
||||
pwd: null,
|
||||
},
|
||||
};
|
||||
|
||||
it('returns existing data if new data is not provided', () => {
|
||||
const mergedData = safeMerge(existing, undefined);
|
||||
expect(mergedData).toEqual(existing);
|
||||
});
|
||||
|
||||
it('merges the rundown key', () => {
|
||||
const newData = {
|
||||
rundown: [{ name: 'item 1' }, { name: 'item 2' }],
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.rundown).toEqual(newData.rundown);
|
||||
});
|
||||
|
||||
it('merges the event key', () => {
|
||||
const newData = {
|
||||
event: {
|
||||
title: 'new title',
|
||||
publicInfo: 'new public info',
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.event).toEqual({
|
||||
title: 'new title',
|
||||
publicUrl: 'existing public URL',
|
||||
publicInfo: 'new public info',
|
||||
backstageUrl: 'existing backstageUrl',
|
||||
backstageInfo: 'existing backstageInfo',
|
||||
endMessage: 'existing endMessage',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the settings key', () => {
|
||||
const newData = {
|
||||
settings: {
|
||||
serverPort: 3000,
|
||||
lock: '1234',
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.settings).toEqual({
|
||||
app: 'ontime',
|
||||
version: 2,
|
||||
serverPort: 3000,
|
||||
lock: '1234',
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the osc key', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
portIn: 7777,
|
||||
subscriptions: {
|
||||
onStart: {
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mergedData = safeMerge(existing, newData);
|
||||
expect(mergedData.osc).toEqual({
|
||||
portIn: 7777,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: 'new message',
|
||||
enabled: true,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the aliases key when present', () => {
|
||||
const existingData = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
backstageUrl: '',
|
||||
backstageInfo: '',
|
||||
endMessage: '',
|
||||
},
|
||||
settings: {
|
||||
app: 'ontime',
|
||||
version: 2,
|
||||
serverPort: 4001,
|
||||
lock: null,
|
||||
pinCode: null,
|
||||
timeFormat: '24',
|
||||
},
|
||||
views: {
|
||||
overrideStyles: false,
|
||||
},
|
||||
aliases: [],
|
||||
userFields: {
|
||||
user0: 'user0',
|
||||
user1: 'user1',
|
||||
user2: 'user2',
|
||||
user3: 'user3',
|
||||
user4: 'user4',
|
||||
user5: 'user5',
|
||||
user6: 'user6',
|
||||
user7: 'user7',
|
||||
user8: 'user8',
|
||||
user9: 'user9',
|
||||
},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
user: null,
|
||||
pwd: null,
|
||||
messages: {
|
||||
onLoad: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
url: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
aliases: ['alias1', 'alias2'],
|
||||
};
|
||||
|
||||
const mergedData = safeMerge(existingData, newData);
|
||||
|
||||
expect(mergedData.aliases).toEqual(newData.aliases);
|
||||
});
|
||||
|
||||
it('merges userFields into existing object', () => {
|
||||
const existing = {
|
||||
userFields: {
|
||||
user0: 'Alice',
|
||||
user1: 'Bob',
|
||||
},
|
||||
};
|
||||
|
||||
const newData = {
|
||||
userFields: {
|
||||
user2: 'Charlie',
|
||||
user3: 'David',
|
||||
user4: null,
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
user0: 'Alice',
|
||||
user1: 'Bob',
|
||||
user2: 'Charlie',
|
||||
user3: 'David',
|
||||
};
|
||||
|
||||
const result = safeMerge(existing, newData);
|
||||
expect(result.userFields).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataProvider } from '../data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../data-provider/DataProvider.ts';
|
||||
import { getRollTimers } from '../../services/rollUtils.js';
|
||||
|
||||
let instance;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as http from 'http';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing HTTP communications
|
||||
* @class
|
||||
*/
|
||||
export class HTTPIntegration {
|
||||
constructor() {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initializes oscClient
|
||||
* @param {object} httpConfig - Http configurations options
|
||||
*/
|
||||
init(httpConfig) {}
|
||||
|
||||
/**
|
||||
* @description Sends http get request from predefined messages
|
||||
* @param {string} path - complete http path
|
||||
*/
|
||||
async send(path) {
|
||||
if (path == null) {
|
||||
console.log('HTTP ERROR: Message undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = new URL(path);
|
||||
let str = '';
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
console.log(`statusCode: ${res.statusCode}`);
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
str += chunk;
|
||||
});
|
||||
|
||||
res.on('end', function () {
|
||||
console.log(str);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
/* Nothing to shutdown */
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import { Client, Message } from 'node-osc';
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OSCIntegration {
|
||||
constructor() {
|
||||
// OSC Client
|
||||
this.ADDRESS = '/ontime';
|
||||
this.oscClient = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Returns list of implemented messages
|
||||
* @returns {object} implemented messages
|
||||
*/
|
||||
get implemented() {
|
||||
return {
|
||||
play: 'play',
|
||||
pause: 'pause',
|
||||
stop: 'stop',
|
||||
previous: 'prev',
|
||||
next: 'next',
|
||||
reload: 'reload',
|
||||
finished: 'finished',
|
||||
time: 'time',
|
||||
overtime: 'overtime',
|
||||
title: 'title',
|
||||
eventNumber: 'eventNumber',
|
||||
presenter: 'presenter',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Initializes oscClient
|
||||
* @param {object} oscConfig - oscClient configuration options
|
||||
* @param {string} oscConfig.ip - oscClient object
|
||||
* @param {number} oscConfig.port - OSC Destination Port
|
||||
*/
|
||||
init(oscConfig) {
|
||||
const { ip, port } = oscConfig;
|
||||
const validateType = typeof ip !== 'string' || typeof port !== 'number';
|
||||
const validateNull = ip == null || port == null;
|
||||
|
||||
if (validateType || validateNull) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Config options incorrect`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.oscClient = new Client(ip, port);
|
||||
return {
|
||||
success: true,
|
||||
message: `Initialised OSC Client at ${ip}:${port}`,
|
||||
};
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed initialising OSC Client: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Sends osc from predefined messages
|
||||
* @param {string} messageType - message to be sent
|
||||
* @param {string} [payload] - optional payload required in some message types
|
||||
*/
|
||||
async send(messageType, payload) {
|
||||
const reply = {
|
||||
success: true,
|
||||
message: 'OSC Message sent',
|
||||
};
|
||||
|
||||
if (this.oscClient == null) {
|
||||
reply.success = false;
|
||||
reply.message = 'Client not initialised';
|
||||
return reply;
|
||||
}
|
||||
|
||||
if (messageType == null) {
|
||||
reply.success = false;
|
||||
reply.message = 'Message undefined';
|
||||
return reply;
|
||||
}
|
||||
|
||||
// only specify special cases
|
||||
switch (payload) {
|
||||
case 'overtime': {
|
||||
// Whether timer is negative
|
||||
this.oscClient.send(`${this.ADDRESS}/overtime`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'title': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send Title of current event
|
||||
this.oscClient.send(`${this.ADDRESS}/title`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'eventNumber': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send event number of current event
|
||||
this.oscClient.send(`${this.ADDRESS}/eventNumber`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'presenter': {
|
||||
if (payload != null && payload !== '') {
|
||||
// Send timer data on current event
|
||||
this.oscClient.send(`${this.ADDRESS}/presenter`, payload, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reply.success = false;
|
||||
reply.message = 'Missing message data';
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// catch all for messages, allows to add new messages
|
||||
// but should be used with the integrations definition
|
||||
const message = new Message(`${this.ADDRESS}/${messageType}`);
|
||||
if (payload != null) message.append(payload);
|
||||
this.oscClient.send(message, (err) => {
|
||||
if (err) {
|
||||
reply.success = false;
|
||||
reply.message = err;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
// Shutdown client object
|
||||
this.oscClient.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
|
||||
import { OSCIntegration } from '../Osc';
|
||||
import { Server } from 'node-osc';
|
||||
|
||||
test('Class initialises correctly', () => {
|
||||
const osc = new OSCIntegration();
|
||||
expect(osc.ADDRESS).toBe('/ontime');
|
||||
expect(osc.oscClient).toBe(null);
|
||||
|
||||
// defined objects
|
||||
expect(osc.implemented.play).toBeDefined();
|
||||
expect(osc.implemented.pause).toBeDefined();
|
||||
expect(osc.implemented.stop).toBeDefined();
|
||||
expect(osc.implemented.previous).toBeDefined();
|
||||
expect(osc.implemented.next).toBeDefined();
|
||||
expect(osc.implemented.reload).toBeDefined();
|
||||
expect(osc.implemented.finished).toBeDefined();
|
||||
expect(osc.implemented.time).toBeDefined();
|
||||
expect(osc.implemented.overtime).toBeDefined();
|
||||
expect(osc.implemented.title).toBeDefined();
|
||||
expect(osc.implemented.eventNumber).toBeDefined();
|
||||
expect(osc.implemented.presenter).toBeDefined();
|
||||
|
||||
// initialise client succeeds
|
||||
const { ip, port } = { ip: '127.0.0.1', port: 12345 };
|
||||
const init = osc.init({ ip, port });
|
||||
expect(init.message).toBe(`Initialised OSC Client at ${ip}:${port}`);
|
||||
expect(init.success).toBe(true);
|
||||
expect(osc.oscClient).not.toBe(null);
|
||||
|
||||
// object shutdown as expected
|
||||
osc.shutdown();
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
describe('OSC fails to initialise when incorrect data is given', () => {
|
||||
it('IP of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 123, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('IP is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: null, port: 8888 });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('Port of wrong type', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: 'test' });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
|
||||
it('Port is null', () => {
|
||||
const osc = new OSCIntegration();
|
||||
const init = osc.init({ ip: 'localhost', port: null });
|
||||
expect(init.message).toBe('Config options incorrect');
|
||||
expect(init.success).toBe(false);
|
||||
expect(osc.oscClient).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
test('Test messages sending', async () => {
|
||||
const testPort = 9999;
|
||||
const testIP = 'localhost';
|
||||
const testPayload = 'test';
|
||||
const osc = new OSCIntegration();
|
||||
|
||||
const messages = [];
|
||||
|
||||
// prepare dummy server to receive messages
|
||||
const oscServer = new Server(testPort, testIP);
|
||||
|
||||
oscServer.on('message', (m) => {
|
||||
messages.push({ yay: m });
|
||||
});
|
||||
|
||||
// try and send a message before initialising
|
||||
const test = await osc.send('test');
|
||||
expect(test.success).toBe(false);
|
||||
expect(test.message).toBe('Client not initialised');
|
||||
|
||||
// initialise osc
|
||||
osc.init({ ip: testIP, port: testPort });
|
||||
|
||||
// try and send unrecognised message
|
||||
const test2 = await osc.send('test');
|
||||
expect(test2.success).toBe(true);
|
||||
|
||||
// send play message
|
||||
const playAddress = osc.implemented.play;
|
||||
const playSent = await osc.send(playAddress);
|
||||
expect(playSent.success).toBe(true);
|
||||
|
||||
// send pause message
|
||||
const pauseAddress = osc.implemented.pause;
|
||||
const pauseSent = await osc.send(pauseAddress);
|
||||
expect(pauseSent.success).toBe(true);
|
||||
|
||||
// send stop message
|
||||
const stopAddress = osc.implemented.stop;
|
||||
const stopSent = await osc.send(stopAddress);
|
||||
expect(stopSent.success).toBe(true);
|
||||
|
||||
// send previous message
|
||||
const previousAddress = osc.implemented.previous;
|
||||
const previousSent = await osc.send(previousAddress);
|
||||
expect(previousSent.success).toBe(true);
|
||||
|
||||
// send next message
|
||||
const nextAddress = osc.implemented.next;
|
||||
const nextSent = await osc.send(nextAddress);
|
||||
expect(nextSent.success).toBe(true);
|
||||
|
||||
// send reload message
|
||||
const reloadAddress = osc.implemented.reload;
|
||||
const reloadSent = await osc.send(reloadAddress);
|
||||
expect(reloadSent.success).toBe(true);
|
||||
|
||||
// send finished message
|
||||
const finishedAddress = osc.implemented.finished;
|
||||
const finishedSent = await osc.send(finishedAddress);
|
||||
expect(finishedSent.success).toBe(true);
|
||||
|
||||
// send time message
|
||||
const timeAddress = osc.implemented.time;
|
||||
const timeSent = await osc.send(timeAddress);
|
||||
expect(timeSent.success).toBe(true);
|
||||
|
||||
// send overtime message
|
||||
const overtimeAddress = osc.implemented.overtime;
|
||||
const overtimeSent = await osc.send(overtimeAddress, testPayload);
|
||||
expect(overtimeSent.success).toBe(true);
|
||||
|
||||
// send title message
|
||||
const titleAddress = osc.implemented.title;
|
||||
const titleSent = await osc.send(titleAddress, testPayload);
|
||||
expect(titleSent.success).toBe(true);
|
||||
|
||||
// send eventNumber message
|
||||
const eventNumberAddress = osc.implemented.eventNumber;
|
||||
const eventNumberSent = await osc.send(eventNumberAddress, testPayload);
|
||||
expect(eventNumberSent.success).toBe(true);
|
||||
|
||||
// send timer message
|
||||
const presenterAddress = osc.implemented.presenter;
|
||||
const presenterSent = await osc.send(presenterAddress, testPayload);
|
||||
expect(presenterSent.success).toBe(true);
|
||||
|
||||
// cleanup
|
||||
await osc.shutdown();
|
||||
await oscServer.close();
|
||||
|
||||
// see messagesObject
|
||||
// expect(messages.length).toBe(5);
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { messageManager } from '../message-manager/MessageManager.js';
|
||||
import { PlaybackService } from '../../services/PlaybackService.js';
|
||||
|
||||
import { ADDRESS_MESSAGE_CONTROL } from './socketConfig.js';
|
||||
import { eventTimer, TimerService } from '../../services/TimerService.js';
|
||||
import { eventTimer, TimerService } from '../../services/TimerService.ts';
|
||||
import { EventLoader, eventLoader } from '../event-loader/EventLoader.js';
|
||||
|
||||
class SocketController {
|
||||
|
||||
@@ -1,25 +1,8 @@
|
||||
export const config = {
|
||||
timer: {
|
||||
refresh: 1000,
|
||||
},
|
||||
server: {
|
||||
port: 4001,
|
||||
},
|
||||
database: {
|
||||
testdb: 'test-db',
|
||||
directory: 'preloaded-db',
|
||||
filename: 'db.json',
|
||||
tablename: 'events',
|
||||
},
|
||||
osc: {
|
||||
port: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
inputEnabled: true,
|
||||
},
|
||||
http: {
|
||||
user: '',
|
||||
pwd: '',
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
+7
-9
@@ -1,4 +1,6 @@
|
||||
import { Server } from 'node-osc';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
import { messageManager } from '../classes/message-manager/MessageManager.js';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
@@ -14,11 +16,10 @@ export const shutdownOSCServer = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* @description initialises OSC server
|
||||
* @param {object} config
|
||||
* Initialises OSC server
|
||||
*/
|
||||
export const initiateOSC = (config) => {
|
||||
oscServer = new Server(config.port, '0.0.0.0');
|
||||
export const initiateOSC = (config: OSCSettings) => {
|
||||
oscServer = new Server(config.portIn, '0.0.0.0');
|
||||
|
||||
oscServer.on('error', console.error);
|
||||
|
||||
@@ -34,7 +35,7 @@ export const initiateOSC = (config) => {
|
||||
|
||||
// get first part before (ontime)
|
||||
if (address !== 'ontime') {
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`);
|
||||
console.error('RX', `OSC IN: Message address ${address} not recognised`, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,10 +124,7 @@ export const initiateOSC = (config) => {
|
||||
try {
|
||||
const eventIndex = Number(args);
|
||||
if (isNaN(eventIndex) || eventIndex <= 0) {
|
||||
socketProvider.error(
|
||||
'RX',
|
||||
`OSC IN: event index not recognised or out of range ${eventIndex}`
|
||||
);
|
||||
socketProvider.error('RX', `OSC IN: event index not recognised or out of range ${eventIndex}`);
|
||||
} else {
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { removeUndefined } from '../utils/parserUtils.js';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
|
||||
// Create controller for GET request to 'event'
|
||||
export const getEvent = async (req, res) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import { networkInterfaces } from 'os';
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { fileHandler } from '../utils/parser.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||
import { mergeObject } from '../utils/parserUtils.js';
|
||||
import { PlaybackService } from '../services/PlaybackService.js';
|
||||
|
||||
@@ -65,10 +65,11 @@ export const validateSettings = [
|
||||
* @description Validates object for POST /ontime/osc
|
||||
*/
|
||||
export const validateOSC = [
|
||||
body('port').exists().isInt({ min: 0, max: 65353 }),
|
||||
body('portOut').exists().isInt({ min: 0, max: 65353 }),
|
||||
body('portIn').exists().isInt({ min: 1024, max: 65535 }),
|
||||
body('portOut').exists().isInt({ min: 1024, max: 65535 }),
|
||||
body('targetIP').exists().isIP(),
|
||||
body('enabled').exists().isBoolean(),
|
||||
body('enabledIn').exists().isBoolean(),
|
||||
body('enabledOut').exists().isBoolean(),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||
import {
|
||||
addEvent,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
// import { promise } from './modules/loadDb.js';
|
||||
import { startOSCServer, startServer } from './app.js';
|
||||
import { startDb, startIntegrations, startOSCServer, startServer } from './app.js';
|
||||
|
||||
async function startOntime() {
|
||||
try {
|
||||
// await promise;
|
||||
await startDb();
|
||||
|
||||
// Start express server
|
||||
const loaded = await startServer();
|
||||
console.log(loaded);
|
||||
|
||||
// Start OSC Server (API)
|
||||
await startOSCServer();
|
||||
await startIntegrations();
|
||||
} catch (error) {
|
||||
console.log('Error starting Ontime');
|
||||
console.log(error);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const dbModel = {
|
||||
import { DatabaseModel } from 'ontime-types';
|
||||
|
||||
export const dbModel: DatabaseModel = {
|
||||
rundown: [],
|
||||
event: {
|
||||
title: '',
|
||||
@@ -33,10 +35,37 @@ export const dbModel = {
|
||||
user9: 'user9',
|
||||
},
|
||||
osc: {
|
||||
port: 8888,
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
targetIP: '127.0.0.1',
|
||||
enabled: true,
|
||||
enabledIn: false,
|
||||
enabledOut: false,
|
||||
subscriptions: {
|
||||
onLoad: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStart: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onPause: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onStop: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onUpdate: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
onFinish: {
|
||||
message: '',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
user: null,
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Low } from 'lowdb';
|
||||
import { JSONFile } from 'lowdb/node';
|
||||
import { copyFileSync, existsSync } from 'fs';
|
||||
import { DatabaseModel } from 'ontime-types';
|
||||
|
||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||
import { validateFile } from '../utils/parserUtils.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
@@ -51,7 +53,7 @@ const parseDb = async (fileToRead, adapterToUse) => {
|
||||
async function loadDb() {
|
||||
const dbInDisk = populateDb();
|
||||
|
||||
const adapter = new JSONFile(dbInDisk);
|
||||
const adapter = new JSONFile<DatabaseModel>(dbInDisk);
|
||||
const db = new Low(adapter);
|
||||
|
||||
const data = await parseDb(dbInDisk, db);
|
||||
@@ -63,11 +65,11 @@ async function loadDb() {
|
||||
}
|
||||
|
||||
export let db = {};
|
||||
export let data = {};
|
||||
export const promise = loadDb();
|
||||
export let data = {} as DatabaseModel;
|
||||
export const dbLoadingProcess = loadDb();
|
||||
|
||||
const init = async () => {
|
||||
const dbProvider = await promise;
|
||||
const dbProvider = await dbLoadingProcess;
|
||||
db = dbProvider.db;
|
||||
data = dbProvider.data;
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
import { eventLoader, EventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer, TimerService } from './TimerService.js';
|
||||
import { eventTimer, TimerService } from './TimerService.ts';
|
||||
|
||||
/**
|
||||
* Service manages playback status of app
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.ts';
|
||||
import {
|
||||
block as blockDef,
|
||||
delay as delayDef,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../models/eventsDefinition.js';
|
||||
import { MAX_EVENTS } from '../settings.js';
|
||||
import { EventLoader, eventLoader } from '../classes/event-loader/EventLoader.js';
|
||||
import { eventTimer } from './TimerService.js';
|
||||
import { eventTimer } from './TimerService.ts';
|
||||
import { socketProvider } from '../classes/socket/SocketController.js';
|
||||
|
||||
/**
|
||||
|
||||
+37
-9
@@ -1,15 +1,40 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { runtimeState } from '../stores/EventStore.js';
|
||||
import { PlaybackService } from './PlaybackService.js';
|
||||
import { updateRoll } from './rollUtils.js';
|
||||
import { DAY_TO_MS } from '../utils/time.js';
|
||||
import { integrationService } from './integration-service/IntegrationService.js';
|
||||
|
||||
export class TimerService {
|
||||
private readonly _interval: NodeJS.Timer;
|
||||
|
||||
private playback: string;
|
||||
|
||||
private loadedTimerId: null;
|
||||
private _pausedInterval: number;
|
||||
private _pausedAt: number | null;
|
||||
private _secondaryTarget: number | null;
|
||||
|
||||
timer: {
|
||||
clock: number;
|
||||
current: number | null;
|
||||
elapsed: number | null;
|
||||
expectedFinish: number | null;
|
||||
addedTime: number;
|
||||
startedAt: number | null;
|
||||
finishedAt: number | null;
|
||||
secondaryTimer: number | null;
|
||||
selectedEventId: string | null;
|
||||
duration: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {object} [timerConfig]
|
||||
* @param {number} [timerConfig.refresh]
|
||||
*/
|
||||
constructor(timerConfig) {
|
||||
constructor(timerConfig?) {
|
||||
this._clear();
|
||||
this._interval = setInterval(() => this.update(), timerConfig?.refresh || 1000);
|
||||
}
|
||||
@@ -45,7 +70,7 @@ export class TimerService {
|
||||
|
||||
return Math.max(
|
||||
this.timer.startedAt + this.timer.duration + this._pausedInterval + this.timer.addedTime,
|
||||
this.timer.startedAt
|
||||
this.timer.startedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,6 +89,8 @@ export class TimerService {
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
secondaryTimer: null,
|
||||
selectedEventId: null,
|
||||
duration: null,
|
||||
};
|
||||
this.loadedTimerId = null;
|
||||
this._pausedInterval = 0;
|
||||
@@ -138,6 +165,7 @@ export class TimerService {
|
||||
_onLoad() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onLoad);
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -172,6 +200,7 @@ export class TimerService {
|
||||
_onStart() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onStart);
|
||||
}
|
||||
|
||||
pause() {
|
||||
@@ -188,6 +217,7 @@ export class TimerService {
|
||||
_onPause() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onPause);
|
||||
}
|
||||
|
||||
stop() {
|
||||
@@ -202,6 +232,7 @@ export class TimerService {
|
||||
_onStop() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onStop);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,8 +280,7 @@ export class TimerService {
|
||||
secondaryTimer: this.timer.secondaryTimer,
|
||||
_secondaryTarget: this._secondaryTarget,
|
||||
};
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } =
|
||||
updateRoll(tempCurrentTimer);
|
||||
const { updatedTimer, updatedSecondaryTimer, doRollLoad, isFinished } = updateRoll(tempCurrentTimer);
|
||||
|
||||
this.timer.current = updatedTimer;
|
||||
this.timer.secondaryTimer = updatedSecondaryTimer;
|
||||
@@ -272,11 +302,7 @@ export class TimerService {
|
||||
}
|
||||
|
||||
this.timer.current =
|
||||
this.timer.startedAt +
|
||||
this.timer.duration +
|
||||
this.timer.addedTime +
|
||||
this._pausedInterval -
|
||||
this.timer.clock;
|
||||
this.timer.startedAt + this.timer.duration + this.timer.addedTime + this._pausedInterval - this.timer.clock;
|
||||
this.timer.elapsed = this.timer.duration - this.timer.current;
|
||||
|
||||
if (this.playback === 'play' && this.timer.current <= 0 && this.timer.finishedAt === null) {
|
||||
@@ -294,11 +320,13 @@ export class TimerService {
|
||||
_onUpdate() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
}
|
||||
|
||||
_onFinish() {
|
||||
runtimeState.set('playback', this.playback);
|
||||
runtimeState.set('ontime-timer', this.timer);
|
||||
integrationService.dispatch(TimerLifeCycle.onFinish);
|
||||
}
|
||||
|
||||
roll(currentEvent, nextEvent, timers) {
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TimerLifeCycle, OscSubscription } from 'ontime-types';
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
export default interface IIntegration {
|
||||
subscriptions: OscSubscription;
|
||||
init: (config: unknown) => OperationReturn;
|
||||
dispatch: (action: TimerLifeCycleKey, state?: object) => OperationReturn;
|
||||
emit: (...args: unknown[]) => unknown;
|
||||
shutdown: () => void;
|
||||
}
|
||||
|
||||
// either went well, or explain what failed
|
||||
type OperationReturn = ReturnOnSuccess | ReturnOnError;
|
||||
|
||||
type ReturnOnSuccess = {
|
||||
success: true;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type ReturnOnError = {
|
||||
success: false;
|
||||
message: string;
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { runtimeState } from '../../stores/EventStore.js';
|
||||
|
||||
class IntegrationService {
|
||||
private integrations: IIntegration[];
|
||||
|
||||
constructor() {
|
||||
this.integrations = [];
|
||||
}
|
||||
|
||||
register(integrationService: IIntegration) {
|
||||
this.integrations.push(integrationService);
|
||||
}
|
||||
|
||||
unregister(integrationService: IIntegration) {
|
||||
this.integrations = this.integrations.filter((int) => int !== integrationService);
|
||||
}
|
||||
|
||||
dispatch(action: TimerLifeCycleKey) {
|
||||
const state = runtimeState.poll();
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.dispatch(action, state);
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
console.log('Shutdown integrations');
|
||||
this.integrations.forEach((integration) => {
|
||||
integration.shutdown();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const integrationService = new IntegrationService();
|
||||
@@ -0,0 +1,118 @@
|
||||
import { ArgumentType, Client, Message } from 'node-osc';
|
||||
import { OSCSettings, OscSubscription } from 'ontime-types';
|
||||
|
||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||
import { parseTemplate } from './integrationUtils.js';
|
||||
import { isObject } from '../../utils/varUtils.js';
|
||||
|
||||
type Action = TimerLifeCycleKey | string;
|
||||
|
||||
/**
|
||||
* @description Class contains logic towards outgoing OSC communications
|
||||
* @class
|
||||
*/
|
||||
export class OscIntegration implements IIntegration {
|
||||
protected oscClient: null | Client;
|
||||
subscriptions: OscSubscription;
|
||||
|
||||
constructor() {
|
||||
this.oscClient = null;
|
||||
this.subscriptions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes oscClient
|
||||
*/
|
||||
init(config: OSCSettings) {
|
||||
const { targetIP, portOut, subscriptions } = config;
|
||||
|
||||
this.initSubscriptions(subscriptions);
|
||||
|
||||
// runtime validation
|
||||
const validateType = typeof targetIP !== 'string' || typeof portOut !== 'number';
|
||||
const validateNull = !targetIP || !portOut;
|
||||
|
||||
if (validateType || validateNull) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Config options incorrect`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.oscClient = new Client(targetIP, portOut);
|
||||
return {
|
||||
success: true,
|
||||
message: `OSC integration client connected to ${targetIP}:${portOut}`,
|
||||
};
|
||||
} catch (error) {
|
||||
this.oscClient = null;
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed initialising OSC Client: ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
initSubscriptions(subscriptionOptions: OscSubscription) {
|
||||
this.subscriptions = { ...this.subscriptions, ...subscriptionOptions };
|
||||
}
|
||||
|
||||
dispatch(action: Action, state?: object) {
|
||||
if (!this.oscClient) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Client not initialised',
|
||||
};
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'OSC called with no action',
|
||||
};
|
||||
}
|
||||
|
||||
// check subscriptions for action
|
||||
const { enabled, message } = this.subscriptions?.[action] || {};
|
||||
if (enabled) {
|
||||
const parsedMessage = parseTemplate(message, state || {});
|
||||
this.emit('address/', parsedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
emit(path: string, payload?: ArgumentType) {
|
||||
const message = new Message(path);
|
||||
if (payload) {
|
||||
try {
|
||||
if (isObject(payload)) {
|
||||
message.append(JSON.stringify(payload));
|
||||
} else {
|
||||
message.append(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('OSC ERROR', error, payload);
|
||||
}
|
||||
}
|
||||
|
||||
this.oscClient.send(message, (error) => {
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `error is here ${JSON.stringify(error)}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: 'OSC Message sent',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
console.log('Shutting down OSC integration');
|
||||
if (this.oscClient) {
|
||||
this.oscClient?.close();
|
||||
this.oscClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { parseTemplate } from './integrationUtils.js';
|
||||
|
||||
describe('integrationUtils', () => {
|
||||
it('correctly parses a given string', () => {
|
||||
const mockState = { test: 'this' };
|
||||
const testString = 'That should replace {{test}}';
|
||||
const expected = `That should replace ${mockState.test}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('parses string with multiple variables', () => {
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('correctly parses a string without templates', () => {
|
||||
const testString = 'That should replace {test}';
|
||||
|
||||
const result = parseTemplate(testString, {});
|
||||
expect(result).toStrictEqual(testString);
|
||||
});
|
||||
|
||||
it('handles scenarios with missing variables', () => {
|
||||
// by failing to provide a value, we give visibility to
|
||||
// potential issues in the given string
|
||||
const mockState = { test1: 'that', test2: 'this' };
|
||||
const testString = '{{test1}} should replace {{test2}}, but not {{test3}}';
|
||||
const expected = `${mockState.test1} should replace ${mockState.test2}, but not {{test3}}`;
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('doesnt yet handle nested variables', () => {
|
||||
const mockState = {
|
||||
timer: {
|
||||
time: '10',
|
||||
},
|
||||
enabled: 'is',
|
||||
};
|
||||
const testString = 'Timer {{enabled}} enabled with {{timer.time}}ms interval';
|
||||
const expected = 'Timer is enabled with {{timer.time}}ms interval';
|
||||
|
||||
const result = parseTemplate(testString, mockState);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// any value inside double curly braces {{val}}
|
||||
const placeholderRegex = /{{(.*?)}}/g;
|
||||
|
||||
/**
|
||||
* Parses a templated string
|
||||
*/
|
||||
export function parseTemplate(template: string, state: object): string {
|
||||
let parsedTemplate = template;
|
||||
let match;
|
||||
while ((match = placeholderRegex.exec(template)) !== null) {
|
||||
const variableName = match[1];
|
||||
if (Object.hasOwn(state, variableName)) {
|
||||
parsedTemplate = parsedTemplate.replace(match[0], state[variableName]);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTemplate;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isObject } from '../varUtils.js';
|
||||
|
||||
describe('isObject', () => {
|
||||
const testCases = [1, 0, false, undefined, 'test', null, () => undefined, []];
|
||||
testCases.forEach((test) => {
|
||||
it(`recognises normal primitives ${test}`, () => {
|
||||
const result = isObject(test);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
-20
@@ -1,4 +1,6 @@
|
||||
import { generateId } from 'ontime-utils';
|
||||
import { OSCSettings } from 'ontime-types';
|
||||
|
||||
import { block as blockDef, delay as delayDef } from '../models/eventsDefinition.js';
|
||||
import { dbModel } from '../models/dataModel.js';
|
||||
import { validateEvent } from './parser.js';
|
||||
@@ -148,31 +150,27 @@ export const parseViews = (data, enforce) => {
|
||||
|
||||
/**
|
||||
* Parse osc portion of an entry
|
||||
* @param {object} data - data object
|
||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
||||
* @returns {object} - event object data
|
||||
*/
|
||||
export const parseOsc = (data, enforce) => {
|
||||
let newOsc = {};
|
||||
export const parseOsc = (
|
||||
data: { osc?: Partial<OSCSettings> },
|
||||
enforce: boolean,
|
||||
): OSCSettings | Record<string, never> => {
|
||||
if ('osc' in data) {
|
||||
console.log('Found OSC definition, importing...');
|
||||
const s = data.osc;
|
||||
const osc = {};
|
||||
|
||||
if (s.port) osc.port = s.port;
|
||||
if (s.portOut) osc.portOut = s.portOut;
|
||||
if (s.targetIP) osc.targetIP = s.targetIP;
|
||||
if (typeof s.enabled !== 'undefined') osc.enabled = s.enabled;
|
||||
// write to db
|
||||
newOsc = {
|
||||
...dbModel.osc,
|
||||
...osc,
|
||||
const loadedConfig = data?.osc || {};
|
||||
return {
|
||||
portIn: loadedConfig.portIn ?? dbModel.osc.portIn,
|
||||
portOut: loadedConfig.portOut ?? dbModel.osc.portOut,
|
||||
targetIP: loadedConfig.targetIP ?? dbModel.osc.targetIP,
|
||||
enabledIn: loadedConfig.enabledIn ?? dbModel.osc.enabledIn,
|
||||
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
|
||||
subscriptions: loadedConfig.subscriptions ?? dbModel.osc.subscriptions,
|
||||
};
|
||||
} else if (enforce) {
|
||||
newOsc = { ...dbModel.osc };
|
||||
console.log(`Created OSC object in db`);
|
||||
}
|
||||
return newOsc;
|
||||
return { ...dbModel.osc };
|
||||
} else return {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -185,18 +183,21 @@ export const parseHttp = (data, enforce) => {
|
||||
const newHttp = {};
|
||||
if ('http' in data) {
|
||||
console.log('Found HTTP definition, importing...');
|
||||
const h = data.osc;
|
||||
const h = data.http;
|
||||
const http = {};
|
||||
|
||||
// @ts-expect-error -- not yet
|
||||
if (h.user) http.user = h.user;
|
||||
// @ts-expect-error -- not yet
|
||||
if (h.pwd) http.pwd = h.pwd;
|
||||
|
||||
// write to db
|
||||
// @ts-expect-error -- not yet
|
||||
newHttp.http = {
|
||||
...dbModel.http,
|
||||
...http,
|
||||
};
|
||||
} else if (enforce) {
|
||||
// @ts-expect-error -- not yet
|
||||
newHttp.http = { ...dbModel.http };
|
||||
console.log(`Created http object in db`);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isObject(variable: unknown): boolean {
|
||||
return typeof variable === 'object' && variable !== null && !Array.isArray(variable);
|
||||
}
|
||||
Reference in New Issue
Block a user