refactor: migrate to OSC min

This commit is contained in:
arc-alex
2025-01-12 13:48:48 +01:00
committed by Carlos Valente
parent bc9345199b
commit f4f266dbd4
8 changed files with 143 additions and 125 deletions
+38 -16
View File
@@ -1,33 +1,51 @@
import { LogOrigin } from 'ontime-types';
import { Server } from 'node-osc';
import { fromBuffer } from 'osc-min';
import * as dgram from 'node:dgram';
import { IAdapter } from './IAdapter.js';
import { logger } from '../classes/Logger.js';
import { integrationPayloadFromPath } from './utils/parse.js';
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
import { isOntimeCloud } from '../externals.js';
export class OscServer implements IAdapter {
private readonly osc: Server;
class OscServer implements IAdapter {
private udpSocket: dgram.Socket | null = null;
constructor(portIn: number) {
this.osc = new Server(portIn, '0.0.0.0');
this.osc.on('error', (error) => logger.error(LogOrigin.Rx, `OSC IN: ${error}`));
this.osc.on('message', (msg) => {
init(port: number) {
if (isOntimeCloud) {
logger.warning(LogOrigin.Rx, 'OSC: Skip starting server in cloud environment');
}
this.udpSocket?.close();
logger.info(LogOrigin.Rx, `OSC: Starting server on port ${port}`);
this.udpSocket = dgram.createSocket('udp4');
this.udpSocket.on('error', (error) => logger.error(LogOrigin.Rx, `OSC IN: ${error}`));
this.udpSocket.on('message', (buf: ArrayBuffer) => {
// message should look like /ontime/{command}/{params?} {args} where
// ontime: fixed message for app
// command: command to be called
// params: used to create a nested object to patch with
// args: extra data, only used on some API entries
// split message
const [, address, command, ...params] = msg[0].split('/');
const args = msg[1];
/**
* TODO: remove this type casting when mergend in deleration file
* https://github.com/DefinitelyTyped/DefinitelyTyped/pull/71659
*/
const msg = fromBuffer(buf);
if (msg.oscType === 'bundle') {
//TODO: manage bundles
logger.error(LogOrigin.Rx, `OSC IN: We don't take bundles`);
return;
}
// get first part before (ontime)
if (address !== 'ontime') {
const { address, args: oscArgs } = msg;
// split message
const [, ontimeKey, command, ...params] = address.split('/');
const args = oscArgs[0]?.value ?? undefined; //TODO: manage multiple args or mayeb we have no usecase
// get first part (ontime)
if (ontimeKey !== 'ontime') {
logger.error(LogOrigin.Rx, `OSC IN: OSC messages to ontime must start with /ontime/, received: ${msg}`);
return;
}
@@ -50,9 +68,13 @@ export class OscServer implements IAdapter {
logger.error(LogOrigin.Rx, `OSC IN: ${error}`);
}
});
this.udpSocket.bind(port);
}
shutdown() {
this.osc?.close();
logger.info(LogOrigin.Rx, `OSC: Closing server`);
this.udpSocket?.close();
this.udpSocket = null;
}
}
export const oscServer = new OscServer();
@@ -113,31 +113,31 @@ describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
const payloads = [
{
test: '"string with space and {{not.so.easy}}"',
expect: [{ type: 'string', value: 'string with space and data with space' }],
expect: { type: 'string', value: 'string with space and data with space' },
},
{
test: '',
expect: [],
expect: undefined,
},
{
test: ' ',
expect: [],
expect: undefined,
},
{
test: '""',
expect: [{ type: 'string', value: '' }],
expect: { type: 'string', value: '' },
},
{
test: '"string with space and {{not.so.empty}}"',
expect: [{ type: 'string', value: 'string with space and ' }],
expect: { type: 'string', value: 'string with space and ' },
},
{
test: '"string with space and {{not.so.number}}"',
expect: [{ type: 'string', value: 'string with space and 1234' }],
expect: { type: 'string', value: 'string with space and 1234' },
},
{
test: '"string with space and {{not.so.stringNumber}}"',
expect: [{ type: 'string', value: 'string with space and 1234' }],
expect: { type: 'string', value: 'string with space and 1234' },
},
{
test: '"{{not.so.easy}}" 1',
@@ -155,7 +155,7 @@ describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
},
{
test: '',
expect: [],
expect: undefined,
},
];
@@ -174,22 +174,28 @@ describe('test stringToOSCArgs()', () => {
{ type: 'string', value: 'test' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
{ type: 'true' },
{ type: 'false' },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
it('empty is nothing', () => {
expect(stringToOSCArgs(undefined)).toStrictEqual([]);
const test = undefined;
const expected = undefined;
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
it('empty is nothing', () => {
expect(stringToOSCArgs('')).toStrictEqual([]);
const test = '';
const expected = undefined;
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
it('1 space is nothing', () => {
expect(stringToOSCArgs(' ')).toStrictEqual([]);
const test = ' ';
const expected = undefined;
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
it('keep other types in strings', () => {
@@ -210,8 +216,8 @@ describe('test stringToOSCArgs()', () => {
{ type: 'string', value: 'test space' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
{ type: 'true' },
{ type: 'false' },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
@@ -222,8 +228,8 @@ describe('test stringToOSCArgs()', () => {
{ type: 'string', value: 'test " space' },
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
{ type: 'true' },
{ type: 'false' },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
@@ -233,8 +239,8 @@ describe('test stringToOSCArgs()', () => {
const expected = [
{ type: 'integer', value: 1111 },
{ type: 'float', value: 0.1111 },
{ type: 'T', value: true },
{ type: 'F', value: false },
{ type: 'true' },
{ type: 'false' },
];
expect(stringToOSCArgs(test)).toStrictEqual(expected);
});
@@ -5,6 +5,7 @@ import type { Request, Response } from 'express';
import * as automationDao from './automation.dao.js';
import * as automationService from './automation.service.js';
import { oscServer } from '../../adapters/OscAdapter.js';
export function getAutomationSettings(_req: Request, res: Response<AutomationSettings>) {
res.json(automationDao.getAutomationSettings());
@@ -20,6 +21,11 @@ export function postAutomationSettings(req: Request, res: Response<AutomationSet
automations: req.body.automations ?? undefined,
blueprints: req.body.blueprints ?? undefined,
});
if (automationSettings.enabledOscIn) {
oscServer.init(automationSettings.oscPortIn);
} else {
oscServer.shutdown();
}
res.status(200).send(automationSettings);
} catch (error) {
const message = getErrorMessage(error);
@@ -1,7 +1,6 @@
import { FilterRule, MaybeNumber } from 'ontime-types';
import { millisToString, removeLeadingZero, splitWhitespace, getPropertyFromPath } from 'ontime-utils';
import { Argument } from 'node-osc';
import type { OscArgOrArrayInput, OscArgInput } from 'osc-min';
type FilterOperator = 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains';
@@ -13,41 +12,44 @@ export function isFilterRule(value: string): value is FilterRule {
return value === 'all' || value === 'any';
}
export function stringToOSCArgs(argsString: string | undefined): Argument[] {
if (typeof argsString === 'undefined' || argsString === '') {
return new Array<Argument>();
function toOscValue(argString: string): OscArgInput {
const argAsNum = Number(argString);
// NOTE: number like: 1 2.0 33333
if (!Number.isNaN(argAsNum)) {
return { type: argString.includes('.') ? 'float' : 'integer', value: argAsNum };
}
if (argString.startsWith('"') && argString.endsWith('"')) {
// NOTE: "quoted string"
return { type: 'string', value: argString.substring(1, argString.length - 1) };
}
if (argString === 'TRUE') {
// NOTE: Boolean true
return { type: 'true' };
}
if (argString === 'FALSE') {
// NOTE: Boolean false
return { type: 'false' };
}
// NOTE: string
return { type: 'string', value: argString };
}
export function stringToOSCArgs(argsString: string | undefined): OscArgInput | OscArgOrArrayInput[] | undefined {
if (typeof argsString === 'undefined' || argsString === '') return;
const matches = splitWhitespace(argsString);
if (!matches) {
return new Array<Argument>();
if (!matches) return;
if (matches.length === 1) {
return toOscValue(matches[0]);
}
const parsedArguments: Argument[] = matches.map((argString: string) => {
const argAsNum = Number(argString);
// NOTE: number like: 1 2.0 33333
if (!Number.isNaN(argAsNum)) {
return { type: argString.includes('.') ? 'float' : 'integer', value: argAsNum };
}
if (argString.startsWith('"') && argString.endsWith('"')) {
// NOTE: "quoted string"
return { type: 'string', value: argString.substring(1, argString.length - 1) };
}
if (argString === 'TRUE') {
// NOTE: Boolean true
return { type: 'T', value: true };
}
if (argString === 'FALSE') {
// NOTE: Boolean false
return { type: 'F', value: false };
}
// NOTE: string
return { type: 'string', value: argString };
});
const parsedArguments: OscArgOrArrayInput[] = matches.map(toOscValue);
return parsedArguments;
}
@@ -1,11 +1,14 @@
import { LogOrigin, OSCOutput } from 'ontime-types';
import { Client, Message } from 'node-osc';
import { type OscPacketInput, toBuffer as oscPacketToBuffer } from 'osc-min';
import * as dgram from 'node:dgram';
import { logger } from '../../../classes/Logger.js';
import { type RuntimeState } from '../../../stores/runtimeState.js';
import { parseTemplateNested, stringToOSCArgs } from '../automation.utils.js';
const udpClient = dgram.createSocket('udp4');
/**
* Expose possibility to send a message using OSC protocol
*/
@@ -15,25 +18,30 @@ export function emitOSC(output: OSCOutput, state: RuntimeState) {
}
/** Parses the state and prepares payload to be emitted */
function preparePayload(output: OSCOutput, state: RuntimeState): Message {
function preparePayload(output: OSCOutput, state: RuntimeState): OscPacketInput {
// check for templates in the address
const parsedAddress = parseTemplateNested(output.address, state);
const message = new Message(parsedAddress);
// check for templates in the arguments
const parsedArguments = output.args ? parseTemplateNested(output.args, state) : undefined;
// check we have the correct type
message.append(stringToOSCArgs(parsedArguments));
return message;
const oscArguments = stringToOSCArgs(parsedArguments);
return { address: parsedAddress, args: oscArguments };
}
/** Emits message over transport */
function emit(targetIP: string, targetPort: number, message: Message) {
logger.info(LogOrigin.Rx, `Sending OSC: ${targetIP}:${targetPort}`);
function emit(targetIP: string, targetPort: number, packet: OscPacketInput) {
logger.info(LogOrigin.Tx, `Sending OSC: ${targetIP}:${targetPort}`);
const oscClient = new Client(targetIP, targetPort);
oscClient.send(message, () => {
oscClient.close();
/**
* TODO: remove this type casting when change is merged
* https://github.com/DefinitelyTyped/DefinitelyTyped/pull/71659
*/
const buffer = oscPacketToBuffer(packet) as unknown as Uint8Array;
udpClient.send(buffer, 0, buffer.byteLength, targetPort, targetIP, (error) => {
if (error) {
logger.warning(LogOrigin.Tx, `Failed sending OSC: ${error}`);
}
});
return;
}
+7 -2
View File
@@ -9,7 +9,7 @@ import cookieParser from 'cookie-parser';
// import utils
import { publicDir, srcDir } from './setup/index.js';
import { environment, isOntimeCloud, isProduction, updateRouterPrefix } from './externals.js';
import { environment, isProduction, updateRouterPrefix } from './externals.js';
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
import { consoleSuccess, consoleHighlight, consoleError } from './utils/console.js';
@@ -37,13 +37,14 @@ import { populateDemo } from './setup/loadDemo.js';
import { getState } from './stores/runtimeState.js';
import { initRundown } from './services/rundown-service/RundownService.js';
import { initialiseProject } from './services/project-service/ProjectService.js';
import { getShowWelcomeDialog } from './services/app-state-service/AppStateService.js';
import { oscServer } from './adapters/OscAdapter.js';
// Utilities
import { clearUploadfolder } from './utils/upload.js';
import { generateCrashReport } from './utils/generateCrashReport.js';
import { timerConfig } from './config/config.js';
import { serverTryDesiredPort, getNetworkInterfaces } from './utils/network.js';
import { getShowWelcomeDialog } from './services/app-state-service/AppStateService.js';
console.log('\n');
consoleHighlight(`Starting Ontime version ${ONTIME_VERSION}`);
@@ -231,6 +232,10 @@ export const startServer = async (
*/
export const startIntegrations = async () => {
checkStart(OntimeStartOrder.InitIO);
const { enabledOscIn, oscPortIn } = getDataProvider().getAutomation();
if (enabledOscIn) {
oscServer.init(oscPortIn);
}
};
/**